summaryrefslogtreecommitdiff
path: root/internal/appconfig
diff options
context:
space:
mode:
Diffstat (limited to 'internal/appconfig')
-rw-r--r--internal/appconfig/app_feature_sections.go40
-rw-r--r--internal/appconfig/app_feature_sections_test.go68
-rw-r--r--internal/appconfig/app_sections.go30
-rw-r--r--internal/appconfig/app_sections_test.go50
-rw-r--r--internal/appconfig/config_features_test.go4
-rw-r--r--internal/appconfig/config_load.go4
-rw-r--r--internal/appconfig/config_types.go8
-rw-r--r--internal/appconfig/feature_sections.go50
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
+}