package app import ( "encoding/json" "fmt" "os" "path/filepath" "github.com/hjbdev/patterm/internal/preset" ) const ( defaultSummaryProvider = "codex" defaultCodexModel = "gpt-5.4-mini" defaultOpenCodeModel = "opencode-go/minimax-m2.7" defaultClaudeModel = "claude-haiku-4-5" ) type settings struct { AutoSummary autoSummarySettings `json:"auto_summary"` } type autoSummarySettings struct { Enabled bool `json:"enabled"` Provider string `json:"provider"` Models map[string]string `json:"models"` Cadence string `json:"cadence"` QuietWindowMS int `json:"quiet_window_ms"` MinInputChars int `json:"min_input_chars"` MaxHistoryChars int `json:"max_history_chars"` } func defaultSettings() settings { return settings{ AutoSummary: autoSummarySettings{ Enabled: true, Provider: defaultSummaryProvider, Models: defaultSummaryModels(), Cadence: "5m", QuietWindowMS: 3000, MinInputChars: 4, MaxHistoryChars: 12000, }, } } func defaultSummaryModels() map[string]string { return map[string]string{ "codex": defaultCodexModel, "opencode": defaultOpenCodeModel, "claude": defaultClaudeModel, } } func loadSettings() (settings, string, error) { base, err := preset.ConfigDir() if err != nil { return settings{}, "", err } path := filepath.Join(base, "settings.json") st := defaultSettings() b, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return st, path, nil } return st, path, fmt.Errorf("settings: read %s: %w", path, err) } if err := json.Unmarshal(b, &st); err != nil { return defaultSettings(), path, fmt.Errorf("settings: parse %s: %w", path, err) } st.normalize() return st, path, nil } func saveSettings(path string, st settings) error { if path == "" { return fmt.Errorf("settings: empty path") } st.normalize() if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return err } b, err := json.MarshalIndent(st, "", " ") if err != nil { return err } b = append(b, '\n') return os.WriteFile(path, b, 0o600) } func (st *settings) normalize() { def := defaultSettings() if st.AutoSummary.Provider == "" { st.AutoSummary.Provider = def.AutoSummary.Provider } switch st.AutoSummary.Provider { case "codex", "opencode", "claude": default: st.AutoSummary.Provider = def.AutoSummary.Provider } if st.AutoSummary.Models == nil { st.AutoSummary.Models = defaultSummaryModels() } else { for k, v := range defaultSummaryModels() { if st.AutoSummary.Models[k] == "" { st.AutoSummary.Models[k] = v } } } if st.AutoSummary.Cadence == "" { st.AutoSummary.Cadence = def.AutoSummary.Cadence } if st.AutoSummary.QuietWindowMS <= 0 { st.AutoSummary.QuietWindowMS = def.AutoSummary.QuietWindowMS } if st.AutoSummary.MinInputChars <= 0 { st.AutoSummary.MinInputChars = def.AutoSummary.MinInputChars } if st.AutoSummary.MaxHistoryChars <= 0 { st.AutoSummary.MaxHistoryChars = def.AutoSummary.MaxHistoryChars } } func (st settings) clone() settings { st.normalize() if st.AutoSummary.Models != nil { models := make(map[string]string, len(st.AutoSummary.Models)) for k, v := range st.AutoSummary.Models { models[k] = v } st.AutoSummary.Models = models } return st } func (a autoSummarySettings) clone() autoSummarySettings { st := settings{AutoSummary: a}.clone() return st.AutoSummary } func (a autoSummarySettings) modelFor(provider string) string { if a.Models == nil { return defaultSummaryModels()[provider] } if m := a.Models[provider]; m != "" { return m } return defaultSummaryModels()[provider] }