diff options
Diffstat (limited to 'internal/appconfig')
| -rw-r--r-- | internal/appconfig/app_feature_sections.go | 40 | ||||
| -rw-r--r-- | internal/appconfig/app_feature_sections_test.go | 68 | ||||
| -rw-r--r-- | internal/appconfig/app_sections.go | 30 | ||||
| -rw-r--r-- | internal/appconfig/app_sections_test.go | 50 | ||||
| -rw-r--r-- | internal/appconfig/config_features_test.go | 4 | ||||
| -rw-r--r-- | internal/appconfig/config_load.go | 4 | ||||
| -rw-r--r-- | internal/appconfig/config_types.go | 8 | ||||
| -rw-r--r-- | internal/appconfig/feature_sections.go | 50 |
8 files changed, 206 insertions, 48 deletions
diff --git a/internal/appconfig/app_feature_sections.go b/internal/appconfig/app_feature_sections.go new file mode 100644 index 0000000..2513946 --- /dev/null +++ b/internal/appconfig/app_feature_sections.go @@ -0,0 +1,40 @@ +package appconfig + +import "slices" + +// This file provides fine-grained, read-only accessors for the individual +// feature subsystems embedded in FeatureConfig. They let a consumer depend on +// just the subsystem config it needs (e.g. the MCP server only needs MCPConfig) +// instead of receiving the whole App God-struct. Each accessor returns a deep +// copy so callers cannot mutate App's internal slices. + +// IgnoreSection returns a copy of the gitignore-aware filtering settings. +func (a *App) IgnoreSection() IgnoreConfig { + c := a.IgnoreConfig + c.IgnoreExtraPatterns = slices.Clone(a.IgnoreExtraPatterns) + return c +} + +// StatsSection returns the usage statistics settings. +func (a *App) StatsSection() StatsConfig { + return a.StatsConfig +} + +// TmuxEditSection returns a copy of the tmux popup editor settings. +func (a *App) TmuxEditSection() TmuxEditConfig { + c := a.TmuxEditConfig + c.TmuxEditAgents = append([]TmuxEditAgentCfg{}, a.TmuxEditAgents...) + return c +} + +// TmuxActionSection returns a copy of the tmux action menu settings. +func (a *App) TmuxActionSection() TmuxActionConfig { + c := a.TmuxActionConfig + c.TmuxActionMenu = append([]TmuxActionMenuEntry{}, a.TmuxActionMenu...) + return c +} + +// MCPSection returns the Model Context Protocol server settings. +func (a *App) MCPSection() MCPConfig { + return a.MCPConfig +} diff --git a/internal/appconfig/app_feature_sections_test.go b/internal/appconfig/app_feature_sections_test.go new file mode 100644 index 0000000..1f9ac2d --- /dev/null +++ b/internal/appconfig/app_feature_sections_test.go @@ -0,0 +1,68 @@ +package appconfig + +import ( + "reflect" + "testing" +) + +// buildFeatureApp returns an App whose FeatureConfig subsystems are all +// populated, used to verify the fine-grained section accessors. +func buildFeatureApp() App { + cfg := App{} + cfg.ApplyFeatureSection(testFeatureConfig()) + return cfg +} + +func TestIgnoreSectionCopiesAndReads(t *testing.T) { + cfg := buildFeatureApp() + got := cfg.IgnoreSection() + if !reflect.DeepEqual(got.IgnoreExtraPatterns, []string{"vendor/**", "tmp/**"}) { + t.Fatalf("IgnoreExtraPatterns = %v", got.IgnoreExtraPatterns) + } + // Mutating the returned copy must not affect the source App. + got.IgnoreExtraPatterns[0] = "mutated" + if cfg.IgnoreExtraPatterns[0] == "mutated" { + t.Fatal("IgnoreSection did not return a defensive copy") + } +} + +func TestStatsSectionReads(t *testing.T) { + cfg := buildFeatureApp() + if got := cfg.StatsSection(); got.StatsWindowMinutes != 15 { + t.Fatalf("StatsWindowMinutes = %d, want 15", got.StatsWindowMinutes) + } +} + +func TestTmuxEditSectionCopies(t *testing.T) { + cfg := buildFeatureApp() + got := cfg.TmuxEditSection() + if got.TmuxEditDefaultAgent != "codex" || len(got.TmuxEditAgents) != 1 { + t.Fatalf("unexpected tmux edit section: %+v", got) + } + got.TmuxEditAgents[0].Name = "mutated" + if cfg.TmuxEditAgents[0].Name == "mutated" { + t.Fatal("TmuxEditSection did not return a defensive copy") + } +} + +func TestTmuxActionSectionCopies(t *testing.T) { + cfg := App{} + cfg.TmuxActionMenu = []TmuxActionMenuEntry{{Kind: "rewrite"}} + got := cfg.TmuxActionSection() + if len(got.TmuxActionMenu) != 1 || got.TmuxActionMenu[0].Kind != "rewrite" { + t.Fatalf("unexpected tmux action section: %+v", got) + } + got.TmuxActionMenu[0].Kind = "mutated" + if cfg.TmuxActionMenu[0].Kind == "mutated" { + t.Fatal("TmuxActionSection did not return a defensive copy") + } +} + +func TestMCPSectionReads(t *testing.T) { + cfg := buildFeatureApp() + got := cfg.MCPSection() + if got.MCPPromptsDir != ".hexai/prompts" || !got.MCPSlashCommandSync || + got.MCPSlashCommandDir != ".hexai/slash" { + t.Fatalf("unexpected MCP section: %+v", got) + } +} diff --git a/internal/appconfig/app_sections.go b/internal/appconfig/app_sections.go index 05b3171..5919db1 100644 --- a/internal/appconfig/app_sections.go +++ b/internal/appconfig/app_sections.go @@ -102,26 +102,18 @@ type PromptConfig struct { TmuxCustomMenuHotkey string `json:"-"` } -// FeatureConfig contains non-LLM feature toggles/integration settings. -// It is embedded in App; fields use json:"-" since features are not exposed via JSON. +// FeatureConfig groups the non-LLM feature subsystems. Rather than a flat +// grab-bag, it now embeds one cohesive struct per subsystem (see +// feature_sections.go). Embedding keeps field promotion intact so existing flat +// access (e.g. cfg.MCPPromptsDir) and the JSON shape are unchanged; the split +// just makes the subsystem boundaries explicit and lets callers depend on a +// single subsystem via the App.*Section accessors. type FeatureConfig struct { - // Stats - StatsWindowMinutes int `json:"-"` - // Ignore: gitignore-aware file filtering for LSP - IgnoreGitignore *bool `json:"-"` - IgnoreExtraPatterns []string `json:"-"` - IgnoreLSPNotify *bool `json:"-"` - // TmuxEdit: popup editor settings for hexai-tmux-edit - TmuxEditPopupWidth string `json:"-"` - TmuxEditPopupHeight string `json:"-"` - TmuxEditDefaultAgent string `json:"-"` - TmuxEditAgents []TmuxEditAgentCfg `json:"-"` - // TmuxAction: configurable main menu for hexai-tmux-action - TmuxActionMenu []TmuxActionMenuEntry `json:"-"` - // MCP: Model Context Protocol server settings - MCPPromptsDir string `json:"-"` // Directory for prompt storage - MCPSlashCommandSync bool `json:"-"` // Enable slash command sync - MCPSlashCommandDir string `json:"-"` // Directory for slash command files + StatsConfig // usage statistics window + IgnoreConfig // gitignore-aware file filtering for LSP + TmuxEditConfig // popup editor settings for hexai-tmux-edit + TmuxActionConfig // configurable main menu for hexai-tmux-action + MCPConfig // Model Context Protocol server settings } // AppSections is the focused split of App into subsystem-specific config groups. diff --git a/internal/appconfig/app_sections_test.go b/internal/appconfig/app_sections_test.go index e26d3f3..bcd1cbe 100644 --- a/internal/appconfig/app_sections_test.go +++ b/internal/appconfig/app_sections_test.go @@ -169,28 +169,34 @@ func testPromptConfig() PromptConfig { func testFeatureConfig() FeatureConfig { return FeatureConfig{ - StatsWindowMinutes: 15, - IgnoreGitignore: sectionBoolPtr(true), - IgnoreExtraPatterns: []string{"vendor/**", "tmp/**"}, - IgnoreLSPNotify: sectionBoolPtr(false), - TmuxEditPopupWidth: "80%", - TmuxEditPopupHeight: "75%", - TmuxEditDefaultAgent: "codex", - TmuxEditAgents: []TmuxEditAgentCfg{{ - Name: "codex", - DisplayName: "Codex", - DetectPattern: "(?i)codex", - SectionPattern: "section", - PromptPattern: "prompt", - StripPatterns: []string{"x", "y"}, - ClearFirst: sectionBoolPtr(true), - ClearKeys: "C-u", - NewlineKeys: "S-Enter", - SubmitKeys: "Enter", - }}, - MCPPromptsDir: ".hexai/prompts", - MCPSlashCommandSync: true, - MCPSlashCommandDir: ".hexai/slash", + StatsConfig: StatsConfig{StatsWindowMinutes: 15}, + IgnoreConfig: IgnoreConfig{ + IgnoreGitignore: sectionBoolPtr(true), + IgnoreExtraPatterns: []string{"vendor/**", "tmp/**"}, + IgnoreLSPNotify: sectionBoolPtr(false), + }, + TmuxEditConfig: TmuxEditConfig{ + TmuxEditPopupWidth: "80%", + TmuxEditPopupHeight: "75%", + TmuxEditDefaultAgent: "codex", + TmuxEditAgents: []TmuxEditAgentCfg{{ + Name: "codex", + DisplayName: "Codex", + DetectPattern: "(?i)codex", + SectionPattern: "section", + PromptPattern: "prompt", + StripPatterns: []string{"x", "y"}, + ClearFirst: sectionBoolPtr(true), + ClearKeys: "C-u", + NewlineKeys: "S-Enter", + SubmitKeys: "Enter", + }}, + }, + MCPConfig: MCPConfig{ + MCPPromptsDir: ".hexai/prompts", + MCPSlashCommandSync: true, + MCPSlashCommandDir: ".hexai/slash", + }, } } diff --git a/internal/appconfig/config_features_test.go b/internal/appconfig/config_features_test.go index 77beb07..2b8c769 100644 --- a/internal/appconfig/config_features_test.go +++ b/internal/appconfig/config_features_test.go @@ -183,13 +183,13 @@ func TestTmuxEditConfig_Merge(t *testing.T) { clearHexaiEnv(t) a := newDefaultConfig() b := App{ - FeatureConfig: FeatureConfig{ + FeatureConfig: FeatureConfig{TmuxEditConfig: TmuxEditConfig{ TmuxEditPopupWidth: "70%", TmuxEditDefaultAgent: "amp", TmuxEditAgents: []TmuxEditAgentCfg{ {Name: "amp", DisplayName: "Amp"}, }, - }, + }}, } a.mergeWith(&b) if a.TmuxEditPopupWidth != "70%" { diff --git a/internal/appconfig/config_load.go b/internal/appconfig/config_load.go index 1e4c6b7..ccbf49f 100644 --- a/internal/appconfig/config_load.go +++ b/internal/appconfig/config_load.go @@ -388,11 +388,11 @@ func applyIgnoreSection(fc *fileConfig, out *App) { if fc.Ignore.Gitignore == nil && len(fc.Ignore.ExtraPatterns) == 0 && fc.Ignore.LSPNotifyIgnored == nil { return } - tmp := App{FeatureConfig: FeatureConfig{ + tmp := App{FeatureConfig: FeatureConfig{IgnoreConfig: IgnoreConfig{ IgnoreGitignore: fc.Ignore.Gitignore, IgnoreExtraPatterns: fc.Ignore.ExtraPatterns, IgnoreLSPNotify: fc.Ignore.LSPNotifyIgnored, - }} + }}} out.mergeBasics(&tmp) } diff --git a/internal/appconfig/config_types.go b/internal/appconfig/config_types.go index 8d50f35..7069b21 100644 --- a/internal/appconfig/config_types.go +++ b/internal/appconfig/config_types.go @@ -106,10 +106,12 @@ func newDefaultConfig() App { }, PromptConfig: defaultPromptConfig(), FeatureConfig: FeatureConfig{ - StatsWindowMinutes: 60, + StatsConfig: StatsConfig{StatsWindowMinutes: 60}, // Ignore: respect .gitignore by default, notify in LSP by default - IgnoreGitignore: boolPtr(true), - IgnoreLSPNotify: boolPtr(true), + IgnoreConfig: IgnoreConfig{ + IgnoreGitignore: boolPtr(true), + IgnoreLSPNotify: boolPtr(true), + }, }, } } diff --git a/internal/appconfig/feature_sections.go b/internal/appconfig/feature_sections.go new file mode 100644 index 0000000..9216400 --- /dev/null +++ b/internal/appconfig/feature_sections.go @@ -0,0 +1,50 @@ +package appconfig + +// This file defines the cohesive per-subsystem config structs that make up +// FeatureConfig. The old FeatureConfig was a grab-bag that mixed five unrelated +// non-LLM subsystems (ignore filtering, stats, tmux popup editor, tmux action +// menu, MCP server). Splitting them into named structs documents the seams +// between subsystems and lets consumers depend on a single subsystem's config +// (via the *Section accessors on App) instead of the whole App God-struct. +// +// Each struct is embedded into FeatureConfig, so Go field promotion keeps the +// historical flat access (e.g. cfg.MCPPromptsDir) working for read sites across +// the codebase. Only composite literals that set these leaf fields directly on +// FeatureConfig had to move to the nested form. + +// IgnoreConfig controls gitignore-aware file filtering for the LSP server. +// Files matching these patterns are skipped for completions and code actions. +type IgnoreConfig struct { + // IgnoreGitignore enables respecting .gitignore entries (default true). + IgnoreGitignore *bool `json:"-"` + // IgnoreExtraPatterns are additional glob patterns to always ignore. + IgnoreExtraPatterns []string `json:"-"` + // IgnoreLSPNotify controls whether the LSP notifies when ignoring a file. + IgnoreLSPNotify *bool `json:"-"` +} + +// StatsConfig holds settings for the usage statistics subsystem. +type StatsConfig struct { + // StatsWindowMinutes is the rolling window (in minutes) used for stats. + StatsWindowMinutes int `json:"-"` +} + +// TmuxEditConfig configures the tmux popup editor feature (hexai-tmux-edit). +type TmuxEditConfig struct { + TmuxEditPopupWidth string `json:"-"` + TmuxEditPopupHeight string `json:"-"` + TmuxEditDefaultAgent string `json:"-"` + TmuxEditAgents []TmuxEditAgentCfg `json:"-"` +} + +// TmuxActionConfig configures the main menu for hexai-tmux-action. +type TmuxActionConfig struct { + TmuxActionMenu []TmuxActionMenuEntry `json:"-"` +} + +// MCPConfig holds Model Context Protocol server settings. +type MCPConfig struct { + MCPPromptsDir string `json:"-"` // Directory for prompt storage + MCPSlashCommandSync bool `json:"-"` // Enable slash command sync + MCPSlashCommandDir string `json:"-"` // Directory for slash command files +} |
