summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-16 03:51:43 +0200
committerPaul Buetow <paul@buetow.org>2026-03-16 03:51:43 +0200
commitde3e878ad12bbd3e609bd5b7d741fc792c72f255 (patch)
tree06d92b93ea0ad532c5d3a761033baac05abe2a5e /internal
parent2e9cabb1c8bf1f0246e513fe1f86a552e07eee94 (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>
Diffstat (limited to 'internal')
-rw-r--r--internal/appconfig/app_sections.go334
-rw-r--r--internal/appconfig/config_features_test.go10
-rw-r--r--internal/appconfig/config_load.go38
-rw-r--r--internal/appconfig/config_types.go184
-rw-r--r--internal/hexaiaction/parse_test.go18
-rw-r--r--internal/hexaiaction/prompts_more_test.go20
-rw-r--r--internal/hexaicli/run_more_test.go2
-rw-r--r--internal/hexaicli/run_output_test.go24
-rw-r--r--internal/hexaicli/run_test.go48
-rw-r--r--internal/hexailsp/run_test.go6
-rw-r--r--internal/hexaimcp/run_test.go26
-rw-r--r--internal/llmutils/client_test.go24
-rw-r--r--internal/lsp/codeaction_custom_test.go18
-rw-r--r--internal/lsp/document_test.go45
-rw-r--r--internal/lsp/ignore_test.go12
-rw-r--r--internal/lsp/server_test.go4
-rw-r--r--internal/lsp/triggers_config_test.go4
-rw-r--r--internal/runtimeconfig/store.go66
-rw-r--r--internal/runtimeconfig/store_test.go54
-rw-r--r--internal/slashcommands/syncer_test.go20
-rw-r--r--internal/tmuxedit/run_test.go6
21 files changed, 458 insertions, 505 deletions
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,
- MaxContextTokens: 4000,
- LogPreviewLimit: 100,
- RequestTimeout: 600,
- CodingTemperature: &t,
- OpenAITemperature: &t,
- OllamaTemperature: &t,
- AnthropicTemperature: &t,
- ManualInvokeMinPrefix: 0,
- CompletionDebounceMs: 800,
- CompletionThrottleMs: 0,
- // Inline/chat trigger defaults
- InlineOpen: ">!",
- InlineClose: ">",
- ChatSuffix: ">",
- ChatPrefixes: []string{"?", "!", ":", ";"},
-
- // Default prompt templates (match current hard-coded strings)
+ CoreConfig: CoreConfig{
+ MaxTokens: 4000,
+ ContextMode: "always-full",
+ ContextWindowLines: 120,
+ MaxContextTokens: 4000,
+ LogPreviewLimit: 100,
+ Request