diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-16 03:51:43 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-16 03:51:43 +0200 |
| commit | de3e878ad12bbd3e609bd5b7d741fc792c72f255 (patch) | |
| tree | 06d92b93ea0ad532c5d3a761033baac05abe2a5e | |
| parent | 2e9cabb1c8bf1f0246e513fe1f86a552e07eee94 (diff) | |
Decompose App God struct into embedded section structs
Replace 60+ flat fields in App with 4 embedded section structs:
CoreConfig, ProviderConfig, PromptConfig, FeatureConfig. Go field
promotion preserves all existing field access patterns. Updated
flattenAppConfig to recurse into embedded structs for runtimeconfig.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
22 files changed, 463 insertions, 508 deletions
diff --git a/cmd/hexai/main_test.go b/cmd/hexai/main_test.go index 7c20cc4..810dcb2 100644 --- a/cmd/hexai/main_test.go +++ b/cmd/hexai/main_test.go @@ -109,9 +109,11 @@ func TestSplitConfigPath(t *testing.T) { func TestPickDefaultModel(t *testing.T) { cfg := appconfig.App{ - OllamaModel: "llama3", - AnthropicModel: "claude-sonnet", - OpenAIModel: "gpt-4o", + ProviderConfig: appconfig.ProviderConfig{ + OllamaModel: "llama3", + AnthropicModel: "claude-sonnet", + OpenAIModel: "gpt-4o", + }, } tests := []struct { provider string diff --git a/internal/appconfig/app_sections.go b/internal/appconfig/app_sections.go index ae60d7a..7422152 100644 --- a/internal/appconfig/app_sections.go +++ b/internal/appconfig/app_sections.go @@ -3,85 +3,119 @@ package appconfig import "slices" // CoreConfig contains core runtime and interaction settings. +// It is embedded in App; JSON tags ensure marshalling works correctly. 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 + MaxTokens int `json:"max_tokens"` + ContextMode string `json:"context_mode"` + ContextWindowLines int `json:"context_window_lines"` + MaxContextTokens int `json:"max_context_tokens"` + LogPreviewLimit int `json:"log_preview_limit"` + RequestTimeout int `json:"request_timeout"` + // Single knob for LSP requests; if set, overrides hardcoded temps in LSP. + CodingTemperature *float64 `json:"coding_temperature"` + // Minimum identifier characters required for manual (TriggerKind=1) invoke + // to proceed without structural triggers. 0 means always allow. + ManualInvokeMinPrefix int `json:"manual_invoke_min_prefix"` + // Completion debounce in milliseconds. When > 0, the server waits until + // there has been no text change for at least this duration before sending + // an LLM completion request. + CompletionDebounceMs int `json:"completion_debounce_ms"` + // Completion throttle in milliseconds. When > 0, caps the minimum spacing + // between LLM requests (both chat and code-completer paths). + CompletionThrottleMs int `json:"completion_throttle_ms"` + // CompletionWaitAll controls whether to wait for all configured completion + // backends before returning results. When true (default), waits for all + // backends. When false, returns the first result immediately. + CompletionWaitAll *bool `json:"completion_wait_all"` + TriggerCharacters []string `json:"trigger_characters"` + Provider string `json:"provider"` + // Inline prompt trigger characters (default: >!text> and >>!text>) + InlineOpen string `json:"inline_open"` + InlineClose string `json:"inline_close"` + // In-editor chat triggers (default: suffix ">" after one of [?, !, :, ;]) + ChatSuffix string `json:"chat_suffix"` + ChatPrefixes []string `json:"chat_prefixes"` } // ProviderConfig contains provider endpoints/models and per-surface model overrides. +// It is embedded in App; JSON tags ensure marshalling works correctly. 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 + // Provider-specific options + OpenAIBaseURL string `json:"openai_base_url"` + OpenAIModel string `json:"openai_model"` + // Default temperature for OpenAI requests (nil means use provider default) + OpenAITemperature *float64 `json:"openai_temperature"` + OpenRouterBaseURL string `json:"openrouter_base_url"` + OpenRouterModel string `json:"openrouter_model"` + // Default temperature for OpenRouter requests (nil means use provider default) + OpenRouterTemperature *float64 `json:"openrouter_temperature"` + OllamaBaseURL string `json:"ollama_base_url"` + OllamaModel string `json:"ollama_model"` + // Default temperature for Ollama requests (nil means use provider default) + OllamaTemperature *float64 `json:"ollama_temperature"` + AnthropicBaseURL string `json:"anthropic_base_url"` + AnthropicModel string `json:"anthropic_model"` + // Default temperature for Anthropic requests (nil means use provider default) + AnthropicTemperature *float64 `json:"anthropic_temperature"` + // Per-surface provider/model configurations (ordered; first entry is primary) + CompletionConfigs []SurfaceConfig `json:"-"` + CodeActionConfigs []SurfaceConfig `json:"-"` + ChatConfigs []SurfaceConfig `json:"-"` + CLIConfigs []SurfaceConfig `json:"-"` } // PromptConfig contains all prompt templates and custom action prompts. +// It is embedded in App; fields use json:"-" since prompts are not exposed via JSON. 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 + // Prompt templates (configured only via file; no env overrides) + // Completion + PromptCompletionSystemGeneral string `json:"-"` + PromptCompletionSystemParams string `json:"-"` + PromptCompletionSystemInline string `json:"-"` + PromptCompletionUserGeneral string `json:"-"` + PromptCompletionUserParams string `json:"-"` + PromptCompletionExtraHeader string `json:"-"` + // Provider-native code-completer + PromptNativeCompletion string `json:"-"` + // In-editor chat + PromptChatSystem string `json:"-"` + // Code actions + PromptCodeActionRewriteSystem string `json:"-"` + PromptCodeActionDiagnosticsSystem string `json:"-"` + PromptCodeActionDocumentSystem string `json:"-"` + PromptCodeActionRewriteUser string `json:"-"` + PromptCodeActionDiagnosticsUser string `json:"-"` + PromptCodeActionDocumentUser string `json:"-"` + PromptCodeActionGoTestSystem string `json:"-"` + PromptCodeActionGoTestUser string `json:"-"` + PromptCodeActionSimplifySystem string `json:"-"` + PromptCodeActionSimplifyUser string `json:"-"` + // CLI + PromptCLIDefaultSystem string `json:"-"` + PromptCLIExplainSystem string `json:"-"` + // Custom code actions and tmux integration + CustomActions []CustomAction `json:"-"` + 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. 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 + // 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:"-"` + // 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 } // AppSections is the focused split of App into subsystem-specific config groups. @@ -110,174 +144,72 @@ func (a *App) ApplySections(sections AppSections) { a.ApplyFeatureSection(sections.Features) } -// CoreSection returns the core runtime and interaction settings. +// CoreSection returns a deep copy of the core runtime and interaction settings. +// Slices are cloned to prevent callers from mutating the original. 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), - } + c := a.CoreConfig + c.TriggerCharacters = slices.Clone(a.TriggerCharacters) + c.ChatPrefixes = slices.Clone(a.ChatPrefixes) + return c } // ApplyCoreSection applies core runtime and interaction settings. +// Slices are cloned to prevent the caller's copy from being shared. 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.CoreConfig = core 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. +// ProviderSection returns a deep copy of provider endpoint/model settings. +// Surface config slices are cloned to prevent callers from mutating the original. 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), - } + p := a.ProviderConfig + p.CompletionConfigs = cloneSurfaceConfigs(a.CompletionConfigs) + p.CodeActionConfigs = cloneSurfaceConfigs(a.CodeActionConfigs) + p.ChatConfigs = cloneSurfaceConfigs(a.ChatConfigs) + p.CLIConfigs = cloneSurfaceConfigs(a.CLIConfigs) + return p } // ApplyProviderSection applies provider endpoint/model settings and surface overrides. +// Surface config slices are cloned to prevent the caller's copy from being shared. 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.ProviderConfig = providers 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. +// PromptSection returns a deep copy of prompt templates and custom action settings. +// The CustomActions slice is cloned to prevent callers from mutating the original. 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, - } + p := a.PromptConfig + p.CustomActions = append([]CustomAction{}, a.CustomActions...) + return p } // ApplyPromptSection applies prompt templates and custom action prompt settings. +// The CustomActions slice is cloned to prevent the caller's copy from being shared. 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.PromptConfig = prompts a.CustomActions = append([]CustomAction{}, prompts.CustomActions...) - a.TmuxCustomMenuHotkey = prompts.TmuxCustomMenuHotkey } -// FeatureSection returns non-LLM feature toggles and integrations. +// FeatureSection returns a deep copy of non-LLM feature toggles and integrations. +// Slices are cloned to prevent callers from mutating the original. 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, - } + f := a.FeatureConfig + f.IgnoreExtraPatterns = slices.Clone(a.IgnoreExtraPatterns) + f.TmuxEditAgents = append([]TmuxEditAgentCfg{}, a.TmuxEditAgents...) + return f } // ApplyFeatureSection applies non-LLM feature toggles and integrations. +// Slices are cloned to prevent the caller's copy from being shared. func (a *App) ApplyFeatureSection(features FeatureConfig) { - a.StatsWindowMinutes = features.StatsWindowMinutes - a.IgnoreGitignore = features.IgnoreGitignore + a.FeatureConfig = features 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/config_features_test.go b/internal/appconfig/config_features_test.go index b3c12e9..9e3528a 100644 --- a/internal/appconfig/config_features_test.go +++ b/internal/appconfig/config_features_test.go @@ -182,10 +182,12 @@ func TestTmuxEditConfig_Merge(t *testing.T) { clearHexaiEnv(t) a := newDefaultConfig() b := App{ - TmuxEditPopupWidth: "70%", - TmuxEditDefaultAgent: "amp", - TmuxEditAgents: []TmuxEditAgentCfg{ - {Name: "amp", DisplayName: "Amp"}, + FeatureConfig: FeatureConfig{ + TmuxEditPopupWidth: "70%", + TmuxEditDefaultAgent: "amp", + TmuxEditAgents: []TmuxEditAgentCfg{ + {Name: "amp", DisplayName: "Amp"}, + }, }, } a.mergeWith(&b) diff --git a/internal/appconfig/config_load.go b/internal/appconfig/config_load.go index dc917ff..37eaca3 100644 --- a/internal/appconfig/config_load.go +++ b/internal/appconfig/config_load.go @@ -290,14 +290,14 @@ func applyGeneralSection(fc *fileConfig, out *App) { if (fc.General == sectionGeneral{}) && fc.General.CodingTemperature == nil { return } - tmp := App{ + tmp := App{CoreConfig: CoreConfig{ MaxTokens: fc.General.MaxTokens, ContextMode: fc.General.ContextMode, ContextWindowLines: fc.General.ContextWindowLines, MaxContextTokens: fc.General.MaxContextTokens, CodingTemperature: fc.General.CodingTemperature, RequestTimeout: fc.General.RequestTimeout, - } + }} out.mergeBasics(&tmp) } @@ -305,7 +305,7 @@ func applyLoggingSection(fc *fileConfig, out *App) { if fc.Logging == (sectionLogging{}) { return } - out.mergeBasics(&App{LogPreviewLimit: fc.Logging.LogPreviewLimit}) + out.mergeBasics(&App{CoreConfig: CoreConfig{LogPreviewLimit: fc.Logging.LogPreviewLimit}}) } func applyCompletionSection(fc *fileConfig, out *App) { @@ -315,12 +315,12 @@ func applyCompletionSection(fc *fileConfig, out *App) { fc.Completion.CompletionWaitAll == nil { return } - tmp := App{ + tmp := App{CoreConfig: CoreConfig{ CompletionDebounceMs: fc.Completion.CompletionDebounceMs, CompletionThrottleMs: fc.Completion.CompletionThrottleMs, ManualInvokeMinPrefix: fc.Completion.ManualInvokeMinPrefix, CompletionWaitAll: fc.Completion.CompletionWaitAll, - } + }} out.mergeBasics(&tmp) } @@ -328,39 +328,39 @@ func applyTriggerSection(fc *fileConfig, out *App) { if len(fc.Triggers.TriggerCharacters) == 0 { return } - out.mergeBasics(&App{TriggerCharacters: fc.Triggers.TriggerCharacters}) + out.mergeBasics(&App{CoreConfig: CoreConfig{TriggerCharacters: fc.Triggers.TriggerCharacters}}) } func applyInlineSection(fc *fileConfig, out *App) { if fc.Inline == (sectionInline{}) { return } - out.mergeBasics(&App{InlineOpen: fc.Inline.InlineOpen, InlineClose: fc.Inline.InlineClose}) + out.mergeBasics(&App{CoreConfig: CoreConfig{InlineOpen: fc.Inline.InlineOpen, InlineClose: fc.Inline.InlineClose}}) } func applyChatSection(fc *fileConfig, out *App) { if strings.TrimSpace(fc.Chat.ChatSuffix) == "" && len(fc.Chat.ChatPrefixes) == 0 { return } - out.mergeBasics(&App{ChatSuffix: fc.Chat.ChatSuffix, ChatPrefixes: fc.Chat.ChatPrefixes}) + out.mergeBasics(&App{CoreConfig: CoreConfig{ChatSuffix: fc.Chat.ChatSuffix, ChatPrefixes: fc.Chat.ChatPrefixes}}) } func applyProviderNameSection(fc *fileConfig, out *App) { if strings.TrimSpace(fc.Provider.Name) == "" { return } - out.mergeBasics(&App{Provider: fc.Provider.Name}) + out.mergeBasics(&App{CoreConfig: CoreConfig{Provider: fc.Provider.Name}}) } func applyIgnoreSection(fc *fileConfig, out *App) { if fc.Ignore.Gitignore == nil && len(fc.Ignore.ExtraPatterns) == 0 && fc.Ignore.LSPNotifyIgnored == nil { return } - tmp := App{ + tmp := App{FeatureConfig: FeatureConfig{ IgnoreGitignore: fc.Ignore.Gitignore, IgnoreExtraPatterns: fc.Ignore.ExtraPatterns, IgnoreLSPNotify: fc.Ignore.LSPNotifyIgnored, - } + }} out.mergeBasics(&tmp) } @@ -368,11 +368,11 @@ func applyOpenAISection(fc *fileConfig, out *App) { if fc.OpenAI.isZero() && fc.OpenAI.Temperature == nil { return } - tmp := App{ + tmp := App{ProviderConfig: ProviderConfig{ OpenAIBaseURL: fc.OpenAI.BaseURL, OpenAIModel: fc.OpenAI.resolvedModel(), OpenAITemperature: fc.OpenAI.Temperature, - } + }} out.mergeProviderFields(&tmp) } @@ -380,11 +380,11 @@ func applyOpenRouterSection(fc *fileConfig, out *App) { if fc.OpenRouter == (sectionOpenRouter{}) && fc.OpenRouter.Temperature == nil { return } - tmp := App{ + tmp := App{ProviderConfig: ProviderConfig{ OpenRouterBaseURL: fc.OpenRouter.BaseURL, OpenRouterModel: fc.OpenRouter.Model, OpenRouterTemperature: fc.OpenRouter.Temperature, - } + }} out.mergeProviderFields(&tmp) } @@ -392,11 +392,11 @@ func applyOllamaSection(fc *fileConfig, out *App) { if fc.Ollama == (sectionOllama{}) && fc.Ollama.Temperature == nil { return } - tmp := App{ + tmp := App{ProviderConfig: ProviderConfig{ OllamaBaseURL: fc.Ollama.BaseURL, OllamaModel: fc.Ollama.Model, OllamaTemperature: fc.Ollama.Temperature, - } + }} out.mergeProviderFields(&tmp) } @@ -404,11 +404,11 @@ func applyAnthropicSection(fc *fileConfig, out *App) { if fc.Anthropic == (sectionAnthropic{}) && fc.Anthropic.Temperature == nil { return } - tmp := App{ + tmp := App{ProviderConfig: ProviderConfig{ AnthropicBaseURL: fc.Anthropic.BaseURL, AnthropicModel: fc.Anthropic.Model, AnthropicTemperature: fc.Anthropic.Temperature, - } + }} out.mergeProviderFields(&tmp) } diff --git a/internal/appconfig/config_types.go b/internal/appconfig/config_types.go index 59b02e3..d7fcc5d 100644 --- a/internal/appconfig/config_types.go +++ b/internal/appconfig/config_types.go @@ -11,114 +11,16 @@ type SurfaceConfig struct { } // App holds user-configurable settings read from ~/.config/hexai/config.toml. +// Fields are organized into embedded section structs. Go promotes all fields, +// so existing code like cfg.MaxTokens continues to work. App is never directly +// TOML-decoded; the fileConfig struct handles TOML parsing and fields are copied +// to App in loadFromFile. JSON marshalling works because section structs carry +// the appropriate json tags. type App struct { - MaxTokens int `json:"max_tokens" toml:"max_tokens"` - ContextMode string `json:"context_mode" toml:"context_mode"` - ContextWindowLines int `json:"context_window_lines" toml:"context_window_lines"` - MaxContextTokens int `json:"max_context_tokens" toml:"max_context_tokens"` - LogPreviewLimit int `json:"log_preview_limit" toml:"log_preview_limit"` - RequestTimeout int `json:"request_timeout" toml:"request_timeout"` - // Single knob for LSP requests; if set, overrides hardcoded temps in LSP. - CodingTemperature *float64 `json:"coding_temperature" toml:"coding_temperature"` - // Minimum identifier characters required for manual (TriggerKind=1) invoke - // to proceed without structural triggers. 0 means always allow. - ManualInvokeMinPrefix int `json:"manual_invoke_min_prefix" toml:"manual_invoke_min_prefix"` - - // Completion debounce in milliseconds. When > 0, the server waits until - // there has been no text change for at least this duration before sending - // an LLM completion request. - CompletionDebounceMs int `json:"completion_debounce_ms" toml:"completion_debounce_ms"` - // Completion throttle in milliseconds. When > 0, caps the minimum spacing - // between LLM requests (both chat and code-completer paths). - CompletionThrottleMs int `json:"completion_throttle_ms" toml:"completion_throttle_ms"` - // CompletionWaitAll controls whether to wait for all configured completion - // backends before returning results. When true (default), waits for all - // backends. When false, returns the first result immediately. - CompletionWaitAll *bool `json:"completion_wait_all" toml:"completion_wait_all"` - - TriggerCharacters []string `json:"trigger_characters" toml:"trigger_characters"` - Provider string `json:"provider" toml:"provider"` - - // Inline prompt trigger characters (default: >!text> and >>!text>) - InlineOpen string `json:"inline_open" toml:"inline_open"` - InlineClose string `json:"inline_close" toml:"inline_close"` - // In-editor chat triggers (default: suffix ">" after one of [?, !, :, ;]) - ChatSuffix string `json:"chat_suffix" toml:"chat_suffix"` - ChatPrefixes []string `json:"chat_prefixes" toml:"chat_prefixes"` - - // Provider-specific options - OpenAIBaseURL string `json:"openai_base_url" toml:"openai_base_url"` - OpenAIModel string `json:"openai_model" toml:"openai_model"` - // Default temperature for OpenAI requests (nil means use provider default) - OpenAITemperature *float64 `json:"openai_temperature" toml:"openai_temperature"` - OpenRouterBaseURL string `json:"openrouter_base_url" toml:"openrouter_base_url"` - OpenRouterModel string `json:"openrouter_model" toml:"openrouter_model"` - // Default temperature for OpenRouter requests (nil means use provider default) - OpenRouterTemperature *float64 `json:"openrouter_temperature" toml:"openrouter_temperature"` - OllamaBaseURL string `json:"ollama_base_url" toml:"ollama_base_url"` - OllamaModel string `json:"ollama_model" toml:"ollama_model"` - // Default temperature for Ollama requests (nil means use provider default) - OllamaTemperature *float64 `json:"ollama_temperature" toml:"ollama_temperature"` - AnthropicBaseURL string `json:"anthropic_base_url" toml:"anthropic_base_url"` - AnthropicModel string `json:"anthropic_model" toml:"anthropic_model"` - // Default temperature for Anthropic requests (nil means use provider default) - AnthropicTemperature *float64 `json:"anthropic_temperature" toml:"anthropic_temperature"` - - // Per-surface provider/model configurations (ordered; first entry is primary) - CompletionConfigs []SurfaceConfig `json:"-" toml:"-"` - CodeActionConfigs []SurfaceConfig `json:"-" toml:"-"` - ChatConfigs []SurfaceConfig `json:"-" toml:"-"` - CLIConfigs []SurfaceConfig `json:"-" toml:"-"` - - // Prompt templates (configured only via file; no env overrides) - // Completion/chat/code action/CLI prompt strings. See config.toml.example for placeholders. - // Completion - PromptCompletionSystemGeneral string `json:"-" toml:"-"` - PromptCompletionSystemParams string `json:"-" toml:"-"` - PromptCompletionSystemInline string `json:"-" toml:"-"` - PromptCompletionUserGeneral string `json:"-" toml:"-"` - PromptCompletionUserParams string `json:"-" toml:"-"` - PromptCompletionExtraHeader string `json:"-" toml:"-"` - // Provider-native code-completer - PromptNativeCompletion string `json:"-" toml:"-"` - // In-editor chat - PromptChatSystem string `json:"-" toml:"-"` - // Code actions - PromptCodeActionRewriteSystem string `json:"-" toml:"-"` - PromptCodeActionDiagnosticsSystem string `json:"-" toml:"-"` - PromptCodeActionDocumentSystem string `json:"-" toml:"-"` - PromptCodeActionRewriteUser string `json:"-" toml:"-"` - PromptCodeActionDiagnosticsUser string `json:"-" toml:"-"` - PromptCodeActionDocumentUser string `json:"-" toml:"-"` - PromptCodeActionGoTestSystem string `json:"-" toml:"-"` - PromptCodeActionGoTestUser string `json:"-" toml:"-"` - PromptCodeActionSimplifySystem string `json:"-" toml:"-"` - PromptCodeActionSimplifyUser string `json:"-" toml:"-"` - // CLI - PromptCLIDefaultSystem string `json:"-" toml:"-"` - PromptCLIExplainSystem string `json:"-" toml:"-"` - - // Custom code actions and tmux integration - CustomActions []CustomAction `json:"-" toml:"-"` - TmuxCustomMenuHotkey string `json:"-" toml:"-"` - // Stats - StatsWindowMinutes int `json:"-" toml:"-"` - - // Ignore: gitignore-aware file filtering for LSP - IgnoreGitignore *bool `json:"-" toml:"-"` - IgnoreExtraPatterns []string `json:"-" toml:"-"` - IgnoreLSPNotify *bool `json:"-" toml:"-"` - - // TmuxEdit: popup editor settings for hexai-tmux-edit - TmuxEditPopupWidth string `json:"-" toml:"-"` - TmuxEditPopupHeight string `json:"-" toml:"-"` - TmuxEditDefaultAgent string `json:"-" toml:"-"` - TmuxEditAgents []TmuxEditAgentCfg `json:"-" toml:"-"` - - // MCP: Model Context Protocol server settings - MCPPromptsDir string `json:"-" toml:"-"` // Directory for prompt storage - MCPSlashCommandSync bool `json:"-" toml:"-"` // Enable slash command sync - MCPSlashCommandDir string `json:"-" toml:"-"` // Directory for slash command files + CoreConfig + ProviderConfig + PromptConfig + FeatureConfig } // CustomAction describes a user-defined code action. @@ -159,32 +61,49 @@ type LoadOptions struct { ProjectRoot string } -// Constructor: defaults for App (kept first among functions) +// Constructor: defaults for App (kept first among functions). +// Initializes via embedded section structs; see CoreConfig, ProviderConfig, +// PromptConfig, and FeatureConfig for field documentation. func newDefaultConfig() App { - // Coding-friendly default temperature across providers + // Coding-friendly default temperature across providers. // Users can override per provider in config.toml (including 0.0). t := 0.2 return App{ - MaxTokens: 4000, - ContextMode: "always-full", - ContextWindowLines: 120, - MaxContextToke |
