diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-02 13:59:38 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-02 13:59:38 +0200 |
| commit | e95ff95d880f9fe13a0efd3d409a19917f4ca476 (patch) | |
| tree | 101a4cb54639e3f48d5a451287e33fa6b97b47c7 /internal | |
| parent | abfb1964fce8f742e1b02d83f4983656c49c7b95 (diff) | |
appconfig: split App into focused section helpers (task 406)
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/appconfig/app_sections.go | 283 | ||||
| -rw-r--r-- | internal/appconfig/app_sections_test.go | 199 |
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 } |
