summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-02 13:59:38 +0200
committerPaul Buetow <paul@buetow.org>2026-03-02 13:59:38 +0200
commite95ff95d880f9fe13a0efd3d409a19917f4ca476 (patch)
tree101a4cb54639e3f48d5a451287e33fa6b97b47c7 /internal
parentabfb1964fce8f742e1b02d83f4983656c49c7b95 (diff)
appconfig: split App into focused section helpers (task 406)
Diffstat (limited to 'internal')
-rw-r--r--internal/appconfig/app_sections.go283
-rw-r--r--internal/appconfig/app_sections_test.go199
2 files changed, 482 insertions, 0 deletions
diff --git a/internal/appconfig/app_sections.go b/internal/appconfig/app_sections.go
new file mode 100644
index 0000000..ae60d7a
--- /dev/null
+++ b/internal/appconfig/app_sections.go
@@ -0,0 +1,283 @@
+package appconfig
+
+import "slices"
+
+// CoreConfig contains core runtime and interaction settings.
+type CoreConfig struct {
+ MaxTokens int
+ ContextMode string
+ ContextWindowLines int
+ MaxContextTokens int
+ LogPreviewLimit int
+ RequestTimeout int
+ CodingTemperature *float64
+ ManualInvokeMinPrefix int
+ CompletionDebounceMs int
+ CompletionThrottleMs int
+ CompletionWaitAll *bool
+ TriggerCharacters []string
+ Provider string
+ InlineOpen string
+ InlineClose string
+ ChatSuffix string
+ ChatPrefixes []string
+}
+
+// ProviderConfig contains provider endpoints/models and per-surface model overrides.
+type ProviderConfig struct {
+ OpenAIBaseURL string
+ OpenAIModel string
+ OpenAITemperature *float64
+ OpenRouterBaseURL string
+ OpenRouterModel string
+ OpenRouterTemperature *float64
+ OllamaBaseURL string
+ OllamaModel string
+ OllamaTemperature *float64
+ AnthropicBaseURL string
+ AnthropicModel string
+ AnthropicTemperature *float64
+ CompletionConfigs []SurfaceConfig
+ CodeActionConfigs []SurfaceConfig
+ ChatConfigs []SurfaceConfig
+ CLIConfigs []SurfaceConfig
+}
+
+// PromptConfig contains all prompt templates and custom action prompts.
+type PromptConfig struct {
+ PromptCompletionSystemGeneral string
+ PromptCompletionSystemParams string
+ PromptCompletionSystemInline string
+ PromptCompletionUserGeneral string
+ PromptCompletionUserParams string
+ PromptCompletionExtraHeader string
+ PromptNativeCompletion string
+ PromptChatSystem string
+ PromptCodeActionRewriteSystem string
+ PromptCodeActionDiagnosticsSystem string
+ PromptCodeActionDocumentSystem string
+ PromptCodeActionRewriteUser string
+ PromptCodeActionDiagnosticsUser string
+ PromptCodeActionDocumentUser string
+ PromptCodeActionGoTestSystem string
+ PromptCodeActionGoTestUser string
+ PromptCodeActionSimplifySystem string
+ PromptCodeActionSimplifyUser string
+ PromptCLIDefaultSystem string
+ PromptCLIExplainSystem string
+ CustomActions []CustomAction
+ TmuxCustomMenuHotkey string
+}
+
+// FeatureConfig contains non-LLM feature toggles/integration settings.
+type FeatureConfig struct {
+ StatsWindowMinutes int
+ IgnoreGitignore *bool
+ IgnoreExtraPatterns []string
+ IgnoreLSPNotify *bool
+ TmuxEditPopupWidth string
+ TmuxEditPopupHeight string
+ TmuxEditDefaultAgent string
+ TmuxEditAgents []TmuxEditAgentCfg
+ MCPPromptsDir string
+ MCPSlashCommandSync bool
+ MCPSlashCommandDir string
+}
+
+// AppSections is the focused split of App into subsystem-specific config groups.
+type AppSections struct {
+ Core CoreConfig
+ Providers ProviderConfig
+ Prompts PromptConfig
+ Features FeatureConfig
+}
+
+// Sections returns the app configuration split into focused sub-configs.
+func (a App) Sections() AppSections {
+ return AppSections{
+ Core: a.CoreSection(),
+ Providers: a.ProviderSection(),
+ Prompts: a.PromptSection(),
+ Features: a.FeatureSection(),
+ }
+}
+
+// ApplySections applies focused sub-config groups back onto App.
+func (a *App) ApplySections(sections AppSections) {
+ a.ApplyCoreSection(sections.Core)
+ a.ApplyProviderSection(sections.Providers)
+ a.ApplyPromptSection(sections.Prompts)
+ a.ApplyFeatureSection(sections.Features)
+}
+
+// CoreSection returns the core runtime and interaction settings.
+func (a App) CoreSection() CoreConfig {
+ return CoreConfig{
+ MaxTokens: a.MaxTokens,
+ ContextMode: a.ContextMode,
+ ContextWindowLines: a.ContextWindowLines,
+ MaxContextTokens: a.MaxContextTokens,
+ LogPreviewLimit: a.LogPreviewLimit,
+ RequestTimeout: a.RequestTimeout,
+ CodingTemperature: a.CodingTemperature,
+ ManualInvokeMinPrefix: a.ManualInvokeMinPrefix,
+ CompletionDebounceMs: a.CompletionDebounceMs,
+ CompletionThrottleMs: a.CompletionThrottleMs,
+ CompletionWaitAll: a.CompletionWaitAll,
+ TriggerCharacters: slices.Clone(a.TriggerCharacters),
+ Provider: a.Provider,
+ InlineOpen: a.InlineOpen,
+ InlineClose: a.InlineClose,
+ ChatSuffix: a.ChatSuffix,
+ ChatPrefixes: slices.Clone(a.ChatPrefixes),
+ }
+}
+
+// ApplyCoreSection applies core runtime and interaction settings.
+func (a *App) ApplyCoreSection(core CoreConfig) {
+ a.MaxTokens = core.MaxTokens
+ a.ContextMode = core.ContextMode
+ a.ContextWindowLines = core.ContextWindowLines
+ a.MaxContextTokens = core.MaxContextTokens
+ a.LogPreviewLimit = core.LogPreviewLimit
+ a.RequestTimeout = core.RequestTimeout
+ a.CodingTemperature = core.CodingTemperature
+ a.ManualInvokeMinPrefix = core.ManualInvokeMinPrefix
+ a.CompletionDebounceMs = core.CompletionDebounceMs
+ a.CompletionThrottleMs = core.CompletionThrottleMs
+ a.CompletionWaitAll = core.CompletionWaitAll
+ a.TriggerCharacters = slices.Clone(core.TriggerCharacters)
+ a.Provider = core.Provider
+ a.InlineOpen = core.InlineOpen
+ a.InlineClose = core.InlineClose
+ a.ChatSuffix = core.ChatSuffix
+ a.ChatPrefixes = slices.Clone(core.ChatPrefixes)
+}
+
+// ProviderSection returns provider endpoint/model settings and surface overrides.
+func (a App) ProviderSection() ProviderConfig {
+ return ProviderConfig{
+ OpenAIBaseURL: a.OpenAIBaseURL,
+ OpenAIModel: a.OpenAIModel,
+ OpenAITemperature: a.OpenAITemperature,
+ OpenRouterBaseURL: a.OpenRouterBaseURL,
+ OpenRouterModel: a.OpenRouterModel,
+ OpenRouterTemperature: a.OpenRouterTemperature,
+ OllamaBaseURL: a.OllamaBaseURL,
+ OllamaModel: a.OllamaModel,
+ OllamaTemperature: a.OllamaTemperature,
+ AnthropicBaseURL: a.AnthropicBaseURL,
+ AnthropicModel: a.AnthropicModel,
+ AnthropicTemperature: a.AnthropicTemperature,
+ CompletionConfigs: cloneSurfaceConfigs(a.CompletionConfigs),
+ CodeActionConfigs: cloneSurfaceConfigs(a.CodeActionConfigs),
+ ChatConfigs: cloneSurfaceConfigs(a.ChatConfigs),
+ CLIConfigs: cloneSurfaceConfigs(a.CLIConfigs),
+ }
+}
+
+// ApplyProviderSection applies provider endpoint/model settings and surface overrides.
+func (a *App) ApplyProviderSection(providers ProviderConfig) {
+ a.OpenAIBaseURL = providers.OpenAIBaseURL
+ a.OpenAIModel = providers.OpenAIModel
+ a.OpenAITemperature = providers.OpenAITemperature
+ a.OpenRouterBaseURL = providers.OpenRouterBaseURL
+ a.OpenRouterModel = providers.OpenRouterModel
+ a.OpenRouterTemperature = providers.OpenRouterTemperature
+ a.OllamaBaseURL = providers.OllamaBaseURL
+ a.OllamaModel = providers.OllamaModel
+ a.OllamaTemperature = providers.OllamaTemperature
+ a.AnthropicBaseURL = providers.AnthropicBaseURL
+ a.AnthropicModel = providers.AnthropicModel
+ a.AnthropicTemperature = providers.AnthropicTemperature
+ a.CompletionConfigs = cloneSurfaceConfigs(providers.CompletionConfigs)
+ a.CodeActionConfigs = cloneSurfaceConfigs(providers.CodeActionConfigs)
+ a.ChatConfigs = cloneSurfaceConfigs(providers.ChatConfigs)
+ a.CLIConfigs = cloneSurfaceConfigs(providers.CLIConfigs)
+}
+
+// PromptSection returns prompt templates and custom action prompt settings.
+func (a App) PromptSection() PromptConfig {
+ return PromptConfig{
+ PromptCompletionSystemGeneral: a.PromptCompletionSystemGeneral,
+ PromptCompletionSystemParams: a.PromptCompletionSystemParams,
+ PromptCompletionSystemInline: a.PromptCompletionSystemInline,
+ PromptCompletionUserGeneral: a.PromptCompletionUserGeneral,
+ PromptCompletionUserParams: a.PromptCompletionUserParams,
+ PromptCompletionExtraHeader: a.PromptCompletionExtraHeader,
+ PromptNativeCompletion: a.PromptNativeCompletion,
+ PromptChatSystem: a.PromptChatSystem,
+ PromptCodeActionRewriteSystem: a.PromptCodeActionRewriteSystem,
+ PromptCodeActionDiagnosticsSystem: a.PromptCodeActionDiagnosticsSystem,
+ PromptCodeActionDocumentSystem: a.PromptCodeActionDocumentSystem,
+ PromptCodeActionRewriteUser: a.PromptCodeActionRewriteUser,
+ PromptCodeActionDiagnosticsUser: a.PromptCodeActionDiagnosticsUser,
+ PromptCodeActionDocumentUser: a.PromptCodeActionDocumentUser,
+ PromptCodeActionGoTestSystem: a.PromptCodeActionGoTestSystem,
+ PromptCodeActionGoTestUser: a.PromptCodeActionGoTestUser,
+ PromptCodeActionSimplifySystem: a.PromptCodeActionSimplifySystem,
+ PromptCodeActionSimplifyUser: a.PromptCodeActionSimplifyUser,
+ PromptCLIDefaultSystem: a.PromptCLIDefaultSystem,
+ PromptCLIExplainSystem: a.PromptCLIExplainSystem,
+ CustomActions: append([]CustomAction{}, a.CustomActions...),
+ TmuxCustomMenuHotkey: a.TmuxCustomMenuHotkey,
+ }
+}
+
+// ApplyPromptSection applies prompt templates and custom action prompt settings.
+func (a *App) ApplyPromptSection(prompts PromptConfig) {
+ a.PromptCompletionSystemGeneral = prompts.PromptCompletionSystemGeneral
+ a.PromptCompletionSystemParams = prompts.PromptCompletionSystemParams
+ a.PromptCompletionSystemInline = prompts.PromptCompletionSystemInline
+ a.PromptCompletionUserGeneral = prompts.PromptCompletionUserGeneral
+ a.PromptCompletionUserParams = prompts.PromptCompletionUserParams
+ a.PromptCompletionExtraHeader = prompts.PromptCompletionExtraHeader
+ a.PromptNativeCompletion = prompts.PromptNativeCompletion
+ a.PromptChatSystem = prompts.PromptChatSystem
+ a.PromptCodeActionRewriteSystem = prompts.PromptCodeActionRewriteSystem
+ a.PromptCodeActionDiagnosticsSystem = prompts.PromptCodeActionDiagnosticsSystem
+ a.PromptCodeActionDocumentSystem = prompts.PromptCodeActionDocumentSystem
+ a.PromptCodeActionRewriteUser = prompts.PromptCodeActionRewriteUser
+ a.PromptCodeActionDiagnosticsUser = prompts.PromptCodeActionDiagnosticsUser
+ a.PromptCodeActionDocumentUser = prompts.PromptCodeActionDocumentUser
+ a.PromptCodeActionGoTestSystem = prompts.PromptCodeActionGoTestSystem
+ a.PromptCodeActionGoTestUser = prompts.PromptCodeActionGoTestUser
+ a.PromptCodeActionSimplifySystem = prompts.PromptCodeActionSimplifySystem
+ a.PromptCodeActionSimplifyUser = prompts.PromptCodeActionSimplifyUser
+ a.PromptCLIDefaultSystem = prompts.PromptCLIDefaultSystem
+ a.PromptCLIExplainSystem = prompts.PromptCLIExplainSystem
+ a.CustomActions = append([]CustomAction{}, prompts.CustomActions...)
+ a.TmuxCustomMenuHotkey = prompts.TmuxCustomMenuHotkey
+}
+
+// FeatureSection returns non-LLM feature toggles and integrations.
+func (a App) FeatureSection() FeatureConfig {
+ return FeatureConfig{
+ StatsWindowMinutes: a.StatsWindowMinutes,
+ IgnoreGitignore: a.IgnoreGitignore,
+ IgnoreExtraPatterns: slices.Clone(a.IgnoreExtraPatterns),
+ IgnoreLSPNotify: a.IgnoreLSPNotify,
+ TmuxEditPopupWidth: a.TmuxEditPopupWidth,
+ TmuxEditPopupHeight: a.TmuxEditPopupHeight,
+ TmuxEditDefaultAgent: a.TmuxEditDefaultAgent,
+ TmuxEditAgents: append([]TmuxEditAgentCfg{}, a.TmuxEditAgents...),
+ MCPPromptsDir: a.MCPPromptsDir,
+ MCPSlashCommandSync: a.MCPSlashCommandSync,
+ MCPSlashCommandDir: a.MCPSlashCommandDir,
+ }
+}
+
+// ApplyFeatureSection applies non-LLM feature toggles and integrations.
+func (a *App) ApplyFeatureSection(features FeatureConfig) {
+ a.StatsWindowMinutes = features.StatsWindowMinutes
+ a.IgnoreGitignore = features.IgnoreGitignore
+ a.IgnoreExtraPatterns = slices.Clone(features.IgnoreExtraPatterns)
+ a.IgnoreLSPNotify = features.IgnoreLSPNotify
+ a.TmuxEditPopupWidth = features.TmuxEditPopupWidth
+ a.TmuxEditPopupHeight = features.TmuxEditPopupHeight
+ a.TmuxEditDefaultAgent = features.TmuxEditDefaultAgent
+ a.TmuxEditAgents = append([]TmuxEditAgentCfg{}, features.TmuxEditAgents...)
+ a.MCPPromptsDir = features.MCPPromptsDir
+ a.MCPSlashCommandSync = features.MCPSlashCommandSync
+ a.MCPSlashCommandDir = features.MCPSlashCommandDir
+}
diff --git a/internal/appconfig/app_sections_test.go b/internal/appconfig/app_sections_test.go
new file mode 100644
index 0000000..e26d3f3
--- /dev/null
+++ b/internal/appconfig/app_sections_test.go
@@ -0,0 +1,199 @@
+package appconfig
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestSectionsRoundTrip(t *testing.T) {
+ expected := App{}
+ expected.ApplySections(testAppSections())
+
+ got := App{}
+ got.ApplySections(expected.Sections())
+
+ if !reflect.DeepEqual(got, expected) {
+ t.Fatalf("round-trip mismatch\n got: %#v\nwant: %#v", got, expected)
+ }
+}
+
+func TestSectionsDefensiveCopies(t *testing.T) {
+ sections := testAppSections()
+ cfg := App{}
+ cfg.ApplySections(sections)
+
+ sections.Core.TriggerCharacters[0] = "mutated"
+ sections.Core.ChatPrefixes[0] = "mutated"
+ sections.Providers.CompletionConfigs[0].Provider = "mutated"
+ sections.Providers.CLIConfigs[0].Model = "mutated"
+ sections.Prompts.CustomActions[0].Title = "mutated"
+ sections.Features.IgnoreExtraPatterns[0] = "mutated"
+ sections.Features.TmuxEditAgents[0].Name = "mutated"
+
+ assertNotEqual(t, cfg.TriggerCharacters[0], "mutated", "trigger characters")
+ assertNotEqual(t, cfg.ChatPrefixes[0], "mutated", "chat prefixes")
+ assertNotEqual(t, cfg.CompletionConfigs[0].Provider, "mutated", "completion configs")
+ assertNotEqual(t, cfg.CLIConfigs[0].Model, "mutated", "cli configs")
+ assertNotEqual(t, cfg.CustomActions[0].Title, "mutated", "custom actions")
+ assertNotEqual(t, cfg.IgnoreExtraPatterns[0], "mutated", "ignore patterns")
+ assertNotEqual(t, cfg.TmuxEditAgents[0].Name, "mutated", "tmux agents")
+
+ out := cfg.Sections()
+ out.Core.TriggerCharacters[0] = "mutated"
+ out.Core.ChatPrefixes[0] = "mutated"
+ out.Providers.CompletionConfigs[0].Provider = "mutated"
+ out.Providers.CLIConfigs[0].Model = "mutated"
+ out.Prompts.CustomActions[0].Title = "mutated"
+ out.Features.IgnoreExtraPatterns[0] = "mutated"
+ out.Features.TmuxEditAgents[0].Name = "mutated"
+
+ assertNotEqual(t, cfg.TriggerCharacters[0], "mutated", "sections trigger characters")
+ assertNotEqual(t, cfg.ChatPrefixes[0], "mutated", "sections chat prefixes")
+ assertNotEqual(t, cfg.CompletionConfigs[0].Provider, "mutated", "sections completion configs")
+ assertNotEqual(t, cfg.CLIConfigs[0].Model, "mutated", "sections cli configs")
+ assertNotEqual(t, cfg.CustomActions[0].Title, "mutated", "sections custom actions")
+ assertNotEqual(t, cfg.IgnoreExtraPatterns[0], "mutated", "sections ignore patterns")
+ assertNotEqual(t, cfg.TmuxEditAgents[0].Name, "mutated", "sections tmux agents")
+}
+
+func assertNotEqual(t *testing.T, got, want, field string) {
+ t.Helper()
+ if got == want {
+ t.Fatalf("%s unexpectedly changed to %q", field, got)
+ }
+}
+
+func testAppSections() AppSections {
+ return AppSections{
+ Core: testCoreConfig(),
+ Providers: testProviderConfig(),
+ Prompts: testPromptConfig(),
+ Features: testFeatureConfig(),
+ }
+}
+
+func testCoreConfig() CoreConfig {
+ return CoreConfig{
+ MaxTokens: 111,
+ ContextMode: "window",
+ ContextWindowLines: 222,
+ MaxContextTokens: 333,
+ LogPreviewLimit: 444,
+ RequestTimeout: 55,
+ CodingTemperature: sectionFloatPtr(0.4),
+ ManualInvokeMinPrefix: 3,
+ CompletionDebounceMs: 700,
+ CompletionThrottleMs: 125,
+ CompletionWaitAll: sectionBoolPtr(false),
+ TriggerCharacters: []string{".", "("},
+ Provider: "openrouter",
+ InlineOpen: "<!",
+ InlineClose: "!>",
+ ChatSuffix: "?",
+ ChatPrefixes: []string{"@", "/"},
+ }
+}
+
+func testProviderConfig() ProviderConfig {
+ return ProviderConfig{
+ OpenAIBaseURL: "https://api.openai.test",
+ OpenAIModel: "gpt-test",
+ OpenAITemperature: sectionFloatPtr(0.6),
+ OpenRouterBaseURL: "https://openrouter.test",
+ OpenRouterModel: "or-test",
+ OpenRouterTemperature: sectionFloatPtr(0.7),
+ OllamaBaseURL: "http://localhost:11434",
+ OllamaModel: "llama3.1",
+ OllamaTemperature: sectionFloatPtr(0.8),
+ AnthropicBaseURL: "https://api.anthropic.test",
+ AnthropicModel: "claude-test",
+ AnthropicTemperature: sectionFloatPtr(0.9),
+ CompletionConfigs: []SurfaceConfig{{
+ Provider: "openai",
+ Model: "gpt-test",
+ Temperature: sectionFloatPtr(0.2),
+ }},
+ CodeActionConfigs: []SurfaceConfig{{
+ Provider: "anthropic",
+ Model: "claude-test",
+ Temperature: sectionFloatPtr(0.3),
+ }},
+ ChatConfigs: []SurfaceConfig{{
+ Provider: "openrouter",
+ Model: "or-test",
+ Temperature: sectionFloatPtr(0.4),
+ }},
+ CLIConfigs: []SurfaceConfig{{
+ Provider: "ollama",
+ Model: "llama3.1",
+ Temperature: sectionFloatPtr(0.5),
+ }},
+ }
+}
+
+func testPromptConfig() PromptConfig {
+ return PromptConfig{
+ PromptCompletionSystemGeneral: "comp sys",
+ PromptCompletionSystemParams: "comp params",
+ PromptCompletionSystemInline: "comp inline",
+ PromptCompletionUserGeneral: "comp user",
+ PromptCompletionUserParams: "comp user params",
+ PromptCompletionExtraHeader: "comp header",
+ PromptNativeCompletion: "native",
+ PromptChatSystem: "chat",
+ PromptCodeActionRewriteSystem: "rewrite sys",
+ PromptCodeActionDiagnosticsSystem: "diag sys",
+ PromptCodeActionDocumentSystem: "doc sys",
+ PromptCodeActionRewriteUser: "rewrite user",
+ PromptCodeActionDiagnosticsUser: "diag user",
+ PromptCodeActionDocumentUser: "doc user",
+ PromptCodeActionGoTestSystem: "gotest sys",
+ PromptCodeActionGoTestUser: "gotest user",
+ PromptCodeActionSimplifySystem: "simplify sys",
+ PromptCodeActionSimplifyUser: "simplify user",
+ PromptCLIDefaultSystem: "cli default",
+ PromptCLIExplainSystem: "cli explain",
+ CustomActions: []CustomAction{{
+ ID: "a1",
+ Title: "Action 1",
+ Kind: "refactor",
+ Scope: "selection",
+ Hotkey: "r",
+ Instruction: "do it",
+ System: "sys",
+ User: "user",
+ }},
+ TmuxCustomMenuHotkey: "M-a",
+ }
+}
+
+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",
+ }
+}
+
+func sectionFloatPtr(v float64) *float64 { return &v }
+
+func sectionBoolPtr(v bool) *bool { return &v }