summaryrefslogtreecommitdiff
path: root/internal/appconfig
diff options
context:
space:
mode:
Diffstat (limited to 'internal/appconfig')
-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
4 files changed, 206 insertions, 360 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,
+ RequestTimeout: 600,
+ CodingTemperature: &t,
+ ManualInvokeMinPrefix: 0,
+ CompletionDebounceMs: 800,
+ CompletionThrottleMs: 0,
+ // Inline/chat trigger defaults
+ InlineOpen: ">!",
+ InlineClose: ">",
+ ChatSuffix: ">",
+ ChatPrefixes: []string{"?", "!", ":", ";"},
+ },
+ ProviderConfig: ProviderConfig{
+ OpenAITemperature: &t,
+ OllamaTemperature: &t,
+ AnthropicTemperature: &t,
+ },
+ PromptConfig: defaultPromptConfig(),
+ FeatureConfig: FeatureConfig{
+ StatsWindowMinutes: 60,
+ // Ignore: respect .gitignore by default, notify in LSP by default
+ IgnoreGitignore: boolPtr(true),
+ IgnoreLSPNotify: boolPtr(true),
+ },
+ }
+}
+
+// defaultPromptConfig returns the default prompt template values.
+func defaultPromptConfig() PromptConfig {
+ return PromptConfig{
PromptCompletionSystemParams: "You are a code completion engine for function signatures. Return only the parameter list contents (without parentheses), no braces, no prose. Prefer idiomatic names and types.",
PromptCompletionUserParams: "Cursor is inside the function parameter list. Suggest only the parameter list (no parentheses).\nFunction line: {{function}}\nCurrent line (cursor at {{char}}): {{current}}",
PromptCompletionSystemGeneral: "You are a terse code completion engine. Return only the code to insert, no surrounding prose or backticks. Only continue from the cursor; never repeat characters already present to the left of the cursor on the current line (e.g., if 'name :=' is already typed, only return the right-hand side expression).",
@@ -209,13 +128,6 @@ func newDefaultConfig() App {
PromptCLIDefaultSystem: "You are Hexai CLI. Default to very short, concise answers. If the user asks for commands, output only the commands (one per line) with no commentary or explanation. Only when the word 'explain' appears in the prompt, produce a verbose explanation.",
PromptCLIExplainSystem: "You are Hexai CLI. The user requested an explanation. Provide a clear, verbose explanation with reasoning and details. If commands are needed, include them with brief context.",
-
- // Stats
- StatsWindowMinutes: 60,
-
- // Ignore: respect .gitignore by default, notify in LSP by default
- IgnoreGitignore: boolPtr(true),
- IgnoreLSPNotify: boolPtr(true),
}
}