Files
patterm/internal/app/settings.go

151 lines
3.7 KiB
Go

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]
}