package offscreen import ( "context" "os" "path/filepath" "strings" "unicode/utf8" ) // goalsPath is the standing-goals note loaded for the off-screen brief. const goalsPath = "~/owc/goals-2026.md" // maxBriefBytes caps the assembled brief so the propose prompt stays bounded. const maxBriefBytes = 8 * 1024 // bugGlob matches the life-domain ("life-bug") notes under ~/owc/resources. const bugGlob = "bug-*.md" // assembleBrief builds the off-screen prompt context from today's tasks, the // standing goals note, and the life-domain notes. It is best-effort: it never // panics and never returns an error, and it tolerates nil ports and missing // files (degrading to a short, mostly-empty brief). func (m *Mode) assembleBrief() string { var b strings.Builder b.WriteString("## Today's tasks\n") b.WriteString(m.briefTasks()) b.WriteString("\n") if goals := m.briefGoals(); goals != "" { b.WriteString("## Goals\n") b.WriteString(goals) b.WriteString("\n\n") } if domains := m.briefLifeDomains(); domains != "" { b.WriteString("## Life domains\n") b.WriteString(domains) b.WriteString("\n") } return truncateBrief(b.String(), maxBriefBytes) } // briefTasks lists today's task titles, or "(none)" when the port is absent, // errors, or returns nothing. func (m *Mode) briefTasks() string { if m.tasks == nil { return "(none)\n" } list, err := m.tasks.Today(context.Background()) if err != nil || len(list) == 0 { return "(none)\n" } var b strings.Builder for _, t := range list { title := strings.TrimSpace(t.Title) if title == "" { continue } b.WriteString("- ") b.WriteString(title) b.WriteString("\n") } if b.Len() == 0 { return "(none)\n" } return b.String() } // briefGoals returns the goals note text, or "" when the port is absent or the // file is missing/empty. func (m *Mode) briefGoals() string { if m.know == nil { return "" } p, err := m.know.Load(context.Background(), goalsPath) if err != nil { return "" } return strings.TrimSpace(p.Text) } // briefLifeDomains loads every ~/owc/resources/bug-*.md note via the knowledge // port (so reads honor its truncation) and concatenates their text under one // section. Returns "" when the port is absent or no notes are readable. File // access stays behind the port; when the port is nil it is skipped entirely. func (m *Mode) briefLifeDomains() string { if m.know == nil { return "" } home, err := os.UserHomeDir() if err != nil { return "" } matches, err := filepath.Glob(filepath.Join(home, "owc", "resources", bugGlob)) if err != nil || len(matches) == 0 { return "" } var b strings.Builder for _, path := range matches { p, err := m.know.Load(context.Background(), path) if err != nil { continue } text := strings.TrimSpace(p.Text) if text == "" { continue } b.WriteString("### ") b.WriteString(filepath.Base(path)) b.WriteString("\n") b.WriteString(text) b.WriteString("\n\n") } return strings.TrimRight(b.String(), "\n") } // truncateBrief clips s to at most max bytes with a marker, keeping the prompt // bounded. It backs up to a rune boundary so it never splits a multibyte rune. func truncateBrief(s string, max int) string { if len(s) <= max { return s } cut := max for cut > 0 && !utf8.RuneStart(s[cut]) { cut-- } return s[:cut] + "\n…(truncated)" }