From 9dc5fc419df4e5ef80594918742b5462d5a4ad4b Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 6 Feb 2026 23:23:57 +0200 Subject: coverage --- docs/coverage.html | 4795 +++++++++++++++++++++++++++++----------------------- 1 file changed, 2677 insertions(+), 2118 deletions(-) (limited to 'docs/coverage.html') diff --git a/docs/coverage.html b/docs/coverage.html index 4526ad1..f0b0e08 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -61,7 +61,7 @@ - + @@ -79,67 +79,69 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + @@ -177,13 +179,13 @@ import ( "codeberg.org/snonux/hexai/internal/hexailsp" ) -func main() { +func main() { logPath := flag.String("log", "/tmp/hexai-lsp.log", "path to log file (optional)") defaultCfg := defaultConfigPath() configPath := flag.String("config", "", fmt.Sprintf("path to config file (default: %s)", defaultCfg)) showVersion := flag.Bool("version", false, "print version and exit") flag.Parse() - if *showVersion { + if *showVersion { log.Println(internal.Version) return } @@ -194,12 +196,12 @@ func main() { } } -func defaultConfigPath() string { +func defaultConfigPath() string { path, err := appconfig.ConfigPath() if err != nil { return "$XDG_CONFIG_HOME/hexai/config.toml" } - return path + return path } @@ -268,34 +270,34 @@ import ( "codeberg.org/snonux/hexai/internal/hexaicli" ) -func main() { +func main() { configPath, remaining := splitConfigPath(os.Args[1:]) logger := log.New(io.Discard, "", 0) cfg := appconfig.LoadWithOptions(logger, appconfig.LoadOptions{ConfigPath: configPath}) cliEntries := cfg.CLIConfigs - if len(cliEntries) == 0 { + if len(cliEntries) == 0 { cliEntries = []appconfig.SurfaceConfig{{Provider: cfg.Provider}} } - fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) + fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) defaultPath := defaultConfigPath() configFlag := fs.String("config", configPath, fmt.Sprintf("path to config file (default: %s)", defaultPath)) showVersion := fs.Bool("version", false, "print version and exit") selectedFlags := make([]bool, len(cliEntries)) - for i, entry := range cliEntries { + for i, entry := range cliEntries { name := strconv.Itoa(i) provider := strings.TrimSpace(entry.Provider) - if provider == "" { + if provider == "" { provider = cfg.Provider } - model := strings.TrimSpace(entry.Model) - if model == "" { + model := strings.TrimSpace(entry.Model) + if model == "" { model = pickDefaultModel(cfg, provider) } - desc := fmt.Sprintf("use only provider #%d (%s:%s)", i, provider, model) + desc := fmt.Sprintf("use only provider #%d (%s:%s)", i, provider, model) fs.BoolVar(&selectedFlags[i], name, false, desc) } - _ = fs.Parse(remaining) - if *showVersion { + _ = fs.Parse(remaining) + if *showVersion { fmt.Fprintln(os.Stdout, internal.Version) return } @@ -321,16 +323,16 @@ func main() { } } -func splitConfigPath(args []string) (string, []string) { +func splitConfigPath(args []string) (string, []string) { var path string rest := make([]string, 0, len(args)) skip := false - for i := 0; i < len(args); i++ { + for i := 0; i < len(args); i++ { if skip { skip = false continue } - arg := args[i] + arg := args[i] switch { case arg == "--config" || arg == "-config": if i+1 < len(args) { @@ -341,30 +343,30 @@ func splitConfigPath(args []string) (string, []string) path = arg[len("-config="):] - default: + default: rest = append(rest, arg) } } - return strings.TrimSpace(path), rest + return strings.TrimSpace(path), rest } -func pickDefaultModel(cfg appconfig.App, provider string) string { +func pickDefaultModel(cfg appconfig.App, provider string) string { switch strings.ToLower(strings.TrimSpace(provider)) { case "ollama": return strings.TrimSpace(cfg.OllamaModel) case "copilot": return strings.TrimSpace(cfg.CopilotModel) - default: + default: return strings.TrimSpace(cfg.OpenAIModel) } } -func defaultConfigPath() string { +func defaultConfigPath() string { cfgPath, err := appconfig.ConfigPath() if err != nil { return "$XDG_CONFIG_HOME/hexai/config.toml" } - return cfgPath + return cfgPath } @@ -397,6 +399,7 @@ type App struct { 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 @@ -410,6 +413,10 @@ type App struct { // 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"` @@ -438,6 +445,10 @@ type App struct { CopilotModel string `json:"copilot_model" toml:"copilot_model"` // Default temperature for Copilot requests (nil means use provider default) CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_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:"-"` @@ -493,7 +504,7 @@ type CustomAction struct { } // Constructor: defaults for App (kept first among functions) -func newDefaultConfig() App { +func newDefaultConfig() App { // Coding-friendly default temperature across providers // Users can override per provider in config.toml (including 0.0). t := 0.2 @@ -503,10 +514,12 @@ func newDefaultConfig() App { ContextWindowLines: 120, MaxContextTokens: 4000, LogPreviewLimit: 100, + RequestTimeout: 30, CodingTemperature: &t, OpenAITemperature: &t, OllamaTemperature: &t, CopilotTemperature: &t, + AnthropicTemperature: &t, ManualInvokeMinPrefix: 0, CompletionDebounceMs: 800, CompletionThrottleMs: 0, @@ -549,7 +562,7 @@ func newDefaultConfig() App { // Load reads configuration from a file and merges with defaults. // It respects the XDG Base Directory Specification. -func Load(logger *log.Logger) App { return LoadWithOptions(logger, LoadOptions{}) } +func Load(logger *log.Logger) App { return LoadWithOptions(logger, LoadOptions{}) } // LoadOptions tune how configuration is loaded at runtime. type LoadOptions struct { @@ -559,35 +572,35 @@ type LoadOptions struct { } // LoadWithOptions reads configuration and applies the requested loading options. -func LoadWithOptions(logger *log.Logger, opts LoadOptions) App { +func LoadWithOptions(logger *log.Logger, opts LoadOptions) App { cfg := newDefaultConfig() - if logger == nil { + if logger == nil { return cfg // Return defaults if no logger is provided (e.g. in tests) } - configPath := strings.TrimSpace(opts.ConfigPath) + configPath := strings.TrimSpace(opts.ConfigPath) if configPath != "" { if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil { cfg.mergeWith(fileCfg) } else if err != nil { logger.Printf("cannot open config file %s: %v", configPath, err) } - } else { + } else { path, err := getConfigPath() if err != nil { logger.Printf("%v", err) - } else if fileCfg, err := loadFromFile(path, logger); err == nil && fileCfg != nil { + } else if fileCfg, err := loadFromFile(path, logger); err == nil && fileCfg != nil { cfg.mergeWith(fileCfg) } } - if !opts.IgnoreEnv { + if !opts.IgnoreEnv { // Environment overrides (take precedence over file) - if envCfg := loadFromEnv(logger); envCfg != nil { + if envCfg := loadFromEnv(logger); envCfg != nil { cfg.mergeWith(envCfg) } } - return cfg + return cfg } // Private helpers @@ -605,6 +618,7 @@ type fileConfig struct { OpenRouter sectionOpenRouter `toml:"openrouter"` Copilot sectionCopilot `toml:"copilot"` Ollama sectionOllama `toml:"ollama"` + Anthropic sectionAnthropic `toml:"anthropic"` Prompts sectionPrompts `toml:"prompts"` Tmux sectionTmux `toml:"tmux"` Stats sectionStats `toml:"stats"` @@ -616,6 +630,7 @@ type sectionGeneral struct { ContextWindowLines int `toml:"context_window_lines"` MaxContextTokens int `toml:"max_context_tokens"` CodingTemperature *float64 `toml:"coding_temperature"` + RequestTimeout int `toml:"request_timeout"` } type sectionLogging struct { @@ -623,9 +638,10 @@ type sectionLogging struct { } type sectionCompletion struct { - CompletionDebounceMs int `toml:"completion_debounce_ms"` - CompletionThrottleMs int `toml:"completion_throttle_ms"` - ManualInvokeMinPrefix int `toml:"manual_invoke_min_prefix"` + CompletionDebounceMs int `toml:"completion_debounce_ms"` + CompletionThrottleMs int `toml:"completion_throttle_ms"` + ManualInvokeMinPrefix int `toml:"manual_invoke_min_prefix"` + CompletionWaitAll *bool `toml:"completion_wait_all"` } type sectionTriggers struct { @@ -657,19 +673,19 @@ type sectionOpenAI struct { Presets map[string]string `toml:"presets"` } -func (s sectionOpenAI) isZero() bool { +func (s sectionOpenAI) isZero() bool { return strings.TrimSpace(s.Model) == "" && strings.TrimSpace(s.BaseURL) == "" && s.Temperature == nil && len(s.Presets) == 0 } -func (s sectionOpenAI) resolvedModel() string { +func (s sectionOpenAI) resolvedModel() string { model := strings.TrimSpace(s.Model) if model == "" { return "" } - if len(s.Presets) == 0 { + if len(s.Presets) == 0 { return model } - if mapped := strings.TrimSpace(s.Presets[model]); mapped != "" { + if mapped := strings.TrimSpace(s.Presets[model]); mapped != "" { return mapped } lower := strings.ToLower(model) @@ -701,6 +717,12 @@ type sectionOllama struct { Temperature *float64 `toml:"temperature"` } +type sectionAnthropic struct { + Model string `toml:"model"` + BaseURL string `toml:"base_url"` + Temperature *float64 `toml:"temperature"` +} + // Prompts sections type sectionPrompts struct { Completion sectionPromptsCompletion `toml:"completion"` @@ -761,63 +783,66 @@ type sectionTmux struct { CustomMenuHotkey string `toml:"custom_menu_hotkey"` } -func (fc *fileConfig) toApp() App { +func (fc *fileConfig) toApp() App { out := App{} // Merge section: general - if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil { + if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil { tmp := App{ 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) } // logging - if (fc.Logging != sectionLogging{}) { + if (fc.Logging != sectionLogging{}) { tmp := App{LogPreviewLimit: fc.Logging.LogPreviewLimit} out.mergeBasics(&tmp) } // completion - if (fc.Completion != sectionCompletion{}) { + if fc.Completion.CompletionDebounceMs != 0 || fc.Completion.CompletionThrottleMs != 0 || + fc.Completion.ManualInvokeMinPrefix != 0 || fc.Completion.CompletionWaitAll != nil { tmp := App{ CompletionDebounceMs: fc.Completion.CompletionDebounceMs, CompletionThrottleMs: fc.Completion.CompletionThrottleMs, ManualInvokeMinPrefix: fc.Completion.ManualInvokeMinPrefix, + CompletionWaitAll: fc.Completion.CompletionWaitAll, } out.mergeBasics(&tmp) } // triggers - if len(fc.Triggers.TriggerCharacters) > 0 { + if len(fc.Triggers.TriggerCharacters) > 0 { tmp := App{TriggerCharacters: fc.Triggers.TriggerCharacters} out.mergeBasics(&tmp) } // inline - if (fc.Inline != sectionInline{}) { + if (fc.Inline != sectionInline{}) { tmp := App{InlineOpen: fc.Inline.InlineOpen, InlineClose: fc.Inline.InlineClose} out.mergeBasics(&tmp) } // chat - if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) > 0 { + if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) > 0 { tmp := App{ChatSuffix: fc.Chat.ChatSuffix, ChatPrefixes: fc.Chat.ChatPrefixes} out.mergeBasics(&tmp) } // provider - if strings.TrimSpace(fc.Provider.Name) != "" { + if strings.TrimSpace(fc.Provider.Name) != "" { tmp := App{Provider: fc.Provider.Name} out.mergeBasics(&tmp) } // openai - if !fc.OpenAI.isZero() || fc.OpenAI.Temperature != nil { + if !fc.OpenAI.isZero() || fc.OpenAI.Temperature != nil { tmp := App{ OpenAIBaseURL: fc.OpenAI.BaseURL, OpenAIModel: fc.OpenAI.resolvedModel(), @@ -827,7 +852,7 @@ func (fc *fileConfig) toApp() App { } // openrouter - if (fc.OpenRouter != sectionOpenRouter{}) || fc.OpenRouter.Temperature != nil { + if (fc.OpenRouter != sectionOpenRouter{}) || fc.OpenRouter.Temperature != nil { tmp := App{ OpenRouterBaseURL: fc.OpenRouter.BaseURL, OpenRouterModel: fc.OpenRouter.Model, @@ -837,7 +862,7 @@ func (fc *fileConfig) toApp() App { } // copilot - if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil { + if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil { tmp := App{ CopilotBaseURL: fc.Copilot.BaseURL, CopilotModel: fc.Copilot.Model, @@ -847,7 +872,7 @@ func (fc *fileConfig) toApp() App { } // ollama - if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil { + if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil { tmp := App{ OllamaBaseURL: fc.Ollama.BaseURL, OllamaModel: fc.Ollama.Model, @@ -856,34 +881,44 @@ func (fc *fileConfig) toApp() App { out.mergeProviderFields(&tmp) } + // anthropic + if (fc.Anthropic != sectionAnthropic{}) || fc.Anthropic.Temperature != nil { + tmp := App{ + AnthropicBaseURL: fc.Anthropic.BaseURL, + AnthropicModel: fc.Anthropic.Model, + AnthropicTemperature: fc.Anthropic.Temperature, + } + out.mergeProviderFields(&tmp) + } + // prompts // completion - if (fc.Prompts.Completion != sectionPromptsCompletion{}) { - if strings.TrimSpace(fc.Prompts.Completion.SystemGeneral) != "" { + if (fc.Prompts.Completion != sectionPromptsCompletion{}) { + if strings.TrimSpace(fc.Prompts.Completion.SystemGeneral) != "" { out.PromptCompletionSystemGeneral = fc.Prompts.Completion.SystemGeneral } - if strings.TrimSpace(fc.Prompts.Completion.SystemParams) != "" { + if strings.TrimSpace(fc.Prompts.Completion.SystemParams) != "" { out.PromptCompletionSystemParams = fc.Prompts.Completion.SystemParams } - if strings.TrimSpace(fc.Prompts.Completion.SystemInline) != "" { + if strings.TrimSpace(fc.Prompts.Completion.SystemInline) != "" { out.PromptCompletionSystemInline = fc.Prompts.Completion.SystemInline } - if strings.TrimSpace(fc.Prompts.Completion.UserGeneral) != "" { + if strings.TrimSpace(fc.Prompts.Completion.UserGeneral) != "" { out.PromptCompletionUserGeneral = fc.Prompts.Completion.UserGeneral } - if strings.TrimSpace(fc.Prompts.Completion.UserParams) != "" { + if strings.TrimSpace(fc.Prompts.Completion.UserParams) != "" { out.PromptCompletionUserParams = fc.Prompts.Completion.UserParams } - if strings.TrimSpace(fc.Prompts.Completion.ExtraHeader) != "" { + if strings.TrimSpace(fc.Prompts.Completion.ExtraHeader) != "" { out.PromptCompletionExtraHeader = fc.Prompts.Completion.ExtraHeader } } // chat - if strings.TrimSpace(fc.Prompts.Chat.System) != "" { + if strings.TrimSpace(fc.Prompts.Chat.System) != "" { out.PromptChatSystem = fc.Prompts.Chat.System } // code action - if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" || + if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" || strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" || strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" || strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" || @@ -893,39 +928,39 @@ func (fc *fileConfig) toApp() App { strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" || strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" || strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" || - len(fc.Prompts.CodeAction.Custom) > 0 { - if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" { + len(fc.Prompts.CodeAction.Custom) > 0 { + if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" { out.PromptCodeActionRewriteSystem = fc.Prompts.CodeAction.RewriteSystem } - if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" { out.PromptCodeActionDiagnosticsSystem = fc.Prompts.CodeAction.DiagnosticsSystem } - if strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" { out.PromptCodeActionDocumentSystem = fc.Prompts.CodeAction.DocumentSystem } - if strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" { out.PromptCodeActionRewriteUser = fc.Prompts.CodeAction.RewriteUser } - if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" { out.PromptCodeActionDiagnosticsUser = fc.Prompts.CodeAction.DiagnosticsUser } - if strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" { out.PromptCodeActionDocumentUser = fc.Prompts.CodeAction.DocumentUser } - if strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" { out.PromptCodeActionGoTestSystem = fc.Prompts.CodeAction.GoTestSystem } - if strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" { out.PromptCodeActionGoTestUser = fc.Prompts.CodeAction.GoTestUser } - if strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" { out.PromptCodeActionSimplifySystem = fc.Prompts.CodeAction.SimplifySystem } - if strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" { + if strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" { out.PromptCodeActionSimplifyUser = fc.Prompts.CodeAction.SimplifyUser } - if len(fc.Prompts.CodeAction.Custom) > 0 { - for _, ca := range fc.Prompts.CodeAction.Custom { + if len(fc.Prompts.CodeAction.Custom) > 0 { + for _, ca := range fc.Prompts.CodeAction.Custom { out.CustomActions = append(out.CustomActions, CustomAction{ ID: strings.TrimSpace(ca.ID), Title: strings.TrimSpace(ca.Title), @@ -940,55 +975,55 @@ func (fc *fileConfig) toApp() App { } } // cli - if (fc.Prompts.CLI != sectionPromptsCLI{}) { - if strings.TrimSpace(fc.Prompts.CLI.DefaultSystem) != "" { + if (fc.Prompts.CLI != sectionPromptsCLI{}) { + if strings.TrimSpace(fc.Prompts.CLI.DefaultSystem) != "" { out.PromptCLIDefaultSystem = fc.Prompts.CLI.DefaultSystem } - if strings.TrimSpace(fc.Prompts.CLI.ExplainSystem) != "" { + if strings.TrimSpace(fc.Prompts.CLI.ExplainSystem) != "" { out.PromptCLIExplainSystem = fc.Prompts.CLI.ExplainSystem } } // provider-native - if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" { + if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" { out.PromptNativeCompletion = fc.Prompts.ProviderNative.Completion } // tmux - if (fc.Tmux != sectionTmux{}) { + if (fc.Tmux != sectionTmux{}) { out.TmuxCustomMenuHotkey = strings.TrimSpace(fc.Tmux.CustomMenuHotkey) } // stats - if fc.Stats.WindowMinutes > 0 { + if fc.Stats.WindowMinutes > 0 { out.StatsWindowMinutes = fc.Stats.WindowMinutes } - return out + return out } -func loadFromFile(path string, logger *log.Logger) (*App, error) { +func loadFromFile(path string, logger *log.Logger) (*App, error) { b, err := os.ReadFile(path) - if err != nil { + if err != nil { if !os.IsNotExist(err) && logger != nil { logger.Printf("cannot open TOML config file %s: %v", path, err) } - return nil, err + return nil, err } - var tables fileConfig + var tables fileConfig errTables := toml.NewDecoder(strings.NewReader(string(b))).Decode(&tables) // Raw map for validation/presence checks var raw map[string]any _ = toml.Unmarshal(b, &raw) - if errTables != nil { - if logger != nil { + if errTables != nil { + if logger != nil { logger.Printf("invalid TOML config file %s: %v", path, errTables) } - return nil, errTables + return nil, errTables } // Reject legacy flat keys at top-level (sectioned-only config is allowed) - legacy := map[string]struct{}{ + legacy := map[string]struct{}{ "max_tokens": {}, "context_mode": {}, "context_window_lines": {}, "max_context_tokens": {}, "log_preview_limit": {}, "completion_debounce_ms": {}, "completion_throttle_ms": {}, "manual_invoke_min_prefix": {}, "trigger_characters": {}, "inline_open": {}, "inline_close": {}, @@ -997,27 +1032,27 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) { - if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "models": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable { + for k := range raw { + if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "models": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable { continue } - if _, isLegacy := legacy[k]; isLegacy { + if _, isLegacy := legacy[k]; isLegacy { return nil, fmt.Errorf("unsupported flat key '%s' in config; use sectioned tables (see config.toml.example)", k) } } - if logger != nil { + if logger != nil { logger.Printf("loaded configuration from %s (TOML)", path) } // Merge order: flat first, then tables (so tables win over zero flat values) // Build App from tables only - tab := tables.toApp() + tab := tables.toApp() // Ensure explicit values from raw map are respected (defensive for ints) - if t, ok := raw["completion"].(map[string]any); ok { - if v, present := t["manual_invoke_min_prefix"]; present { + if t, ok := raw["completion"].(map[string]any); ok { + if v, present := t["manual_invoke_min_prefix"]; present { switch vv := v.(type) { - case int64: + case int64: tab.ManualInvokeMinPrefix = int(vv) case int: tab.ManualInvokeMinPrefix = vv @@ -1026,10 +1061,10 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) if t, ok := raw["logging"].(map[string]any); ok { - if v, present := t["log_preview_limit"]; present { + if t, ok := raw["logging"].(map[string]any); ok { + if v, present := t["log_preview_limit"]; present { switch vv := v.(type) { - case int64: + case int64: tab.LogPreviewLimit = int(vv) case int: tab.LogPreviewLimit = vv @@ -1038,65 +1073,65 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) if m := parseSurfaceModels(raw, logger); m != nil { + if m := parseSurfaceModels(raw, logger); m != nil { tab.mergeSurfaceModels(m) } - return &tab, nil + return &tab, nil } -func parseSurfaceModels(raw map[string]any, logger *log.Logger) *App { +func parseSurfaceModels(raw map[string]any, logger *log.Logger) *App { modelsRaw, ok := raw["models"] - if !ok { + if !ok { return nil } - table, ok := modelsRaw.(map[string]any) + table, ok := modelsRaw.(map[string]any) if !ok { if logger != nil { logger.Printf("config: ignoring models section (expected table, got %T)", modelsRaw) } return nil } - var out App - appendEntries := func(dest *[]SurfaceConfig, key string, val any) bool { + var out App + appendEntries := func(dest *[]SurfaceConfig, key string, val any) bool { entries, ok := parseSurfaceEntries(val, key, logger) - if !ok || len(entries) == 0 { + if !ok || len(entries) == 0 { return false } - *dest = append(*dest, entries...) + *dest = append(*dest, entries...) return true } - any := appendEntries(&out.CompletionConfigs, "models.completion", table["completion"]) - if ok := appendEntries(&out.CodeActionConfigs, "models.code_action", table["code_action"]); ok { - if len(out.CodeActionConfigs) > 1 { - if logger != nil { + any := appendEntries(&out.CompletionConfigs, "models.completion", table["completion"]) + if ok := appendEntries(&out.CodeActionConfigs, "models.code_action", table["code_action"]); ok { + if len(out.CodeActionConfigs) > 1 { + if logger != nil { logger.Printf("config: models.code_action supports a single entry; ignoring %d extra", len(out.CodeActionConfigs)-1) } - out.CodeActionConfigs = out.CodeActionConfigs[:1] + out.CodeActionConfigs = out.CodeActionConfigs[:1] } - any = true + any = true } - any = appendEntries(&out.ChatConfigs, "models.chat", table["chat"]) || any + any = appendEntries(&out.ChatConfigs, "models.chat", table["chat"]) || any any = appendEntries(&out.CLIConfigs, "models.cli", table["cli"]) || any if !any { return nil } - return &out + return &out } -func parseSurfaceEntries(raw any, path string, logger *log.Logger) ([]SurfaceConfig, bool) { +func parseSurfaceEntries(raw any, path string, logger *log.Logger) ([]SurfaceConfig, bool) { switch v := raw.(type) { - case nil: + case nil: return nil, false - case []any: + case []any: var out []SurfaceConfig - for i, entry := range v { + for i, entry := range v { cfg, ok := decodeModelEntry(entry, fmt.Sprintf("%s[%d]", path, i), logger) if !ok || cfg == nil { continue } - out = append(out, *cfg) + out = append(out, *cfg) } - return out, len(out) > 0 + return out, len(out) > 0 default: if cfg, ok := decodeModelEntry(v, path, logger); ok && cfg != nil { return []SurfaceConfig{*cfg}, true @@ -1105,30 +1140,30 @@ func parseSurfaceEntries(raw any, path string, logger *log.Logger) ([]SurfaceCon } } -func cloneSurfaceConfigs(src []SurfaceConfig) []SurfaceConfig { +func cloneSurfaceConfigs(src []SurfaceConfig) []SurfaceConfig { if len(src) == 0 { return nil } - out := make([]SurfaceConfig, len(src)) + out := make([]SurfaceConfig, len(src)) copy(out, src) return out } -func decodeModelEntry(raw any, path string, logger *log.Logger) (*SurfaceConfig, bool) { +func decodeModelEntry(raw any, path string, logger *log.Logger) (*SurfaceConfig, bool) { if raw == nil { return nil, false } - switch v := raw.(type) { + switch v := raw.(type) { case string: model := strings.TrimSpace(v) if model == "" { return nil, false } return &SurfaceConfig{Model: model}, true - case map[string]any: + case map[string]any: model := "" provider := "" - if m, ok := v["model"]; ok { + if m, ok := v["model"]; ok { s, ok := m.(string) if !ok { if logger != nil { @@ -1136,9 +1171,9 @@ func decodeModelEntry(raw any, path string, logger *log.Logger) (*SurfaceConfig, } return nil, false } - model = strings.TrimSpace(s) + model = strings.TrimSpace(s) } - if pRaw, ok := v["provider"]; ok { + if pRaw, ok := v["provider"]; ok { ps, ok := pRaw.(string) if !ok { if logger != nil { @@ -1146,20 +1181,20 @@ func decodeModelEntry(raw any, path string, logger *log.Logger) (*SurfaceConfig, } return nil, false } - provider = strings.TrimSpace(ps) + provider = strings.TrimSpace(ps) } - var tempPtr *float64 - if tRaw, ok := v["temperature"]; ok { + var tempPtr *float64 + if tRaw, ok := v["temperature"]; ok { parsed, ok := parseTemperatureValue(tRaw, path, logger) if !ok { return nil, false } - tempPtr = parsed + tempPtr = parsed } - if model == "" && tempPtr == nil && provider == "" { + if model == "" && tempPtr == nil && provider == "" { return nil, false } - return &SurfaceConfig{Provider: provider, Model: model, Temperature: tempPtr}, true + return &SurfaceConfig{Provider: provider, Model: model, Temperature: tempPtr}, true default: if logger != nil { logger.Printf("config: %s must be a string or table, got %T", path, raw) @@ -1168,9 +1203,9 @@ func decodeModelEntry(raw any, path string, logger *log.Logger) (*SurfaceConfig, } } -func parseTemperatureValue(raw any, path string, logger *log.Logger) (*float64, bool) { +func parseTemperatureValue(raw any, path string, logger *log.Logger) (*float64, bool) { switch v := raw.(type) { - case float64: + case float64: return floatPtr(v), true case int64: return floatPtr(float64(v)), true @@ -1195,12 +1230,12 @@ func parseTemperatureValue(raw any, path string, logger *log.Logger) (*float64, } } -func floatPtr(v float64) *float64 { +func floatPtr(v float64) *float64 { f := v return &f } -func (a *App) mergeWith(other *App) { +func (a *App) mergeWith(other *App) { a.mergeBasics(other) a.mergeProviderFields(other) a.mergeSurfaceModels(other) @@ -1208,360 +1243,370 @@ func (a *App) mergeWith(other *App) { } // mergeBasics merges general (non-provider) fields. -func (a *App) mergeBasics(other *App) { - if other.MaxTokens > 0 { +func (a *App) mergeBasics(other *App) { + if other.MaxTokens > 0 { a.MaxTokens = other.MaxTokens } - if s := strings.TrimSpace(other.ContextMode); s != "" { + if s := strings.TrimSpace(other.ContextMode); s != "" { a.ContextMode = s } - if other.ContextWindowLines > 0 { + if other.ContextWindowLines > 0 { a.ContextWindowLines = other.ContextWindowLines } - if other.MaxContextTokens > 0 { + if other.MaxContextTokens > 0 { a.MaxContextTokens = other.MaxContextTokens } - if other.LogPreviewLimit >= 0 { + if other.LogPreviewLimit >= 0 { a.LogPreviewLimit = other.LogPreviewLimit } - if other.CodingTemperature != nil { // allow explicit 0.0 + if other.RequestTimeout > 0 { + a.RequestTimeout = other.RequestTimeout + } + if other.CodingTemperature != nil { // allow explicit 0.0 a.CodingTemperature = other.CodingTemperature } - if other.ManualInvokeMinPrefix >= 0 { + if other.ManualInvokeMinPrefix >= 0 { a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix } - if other.CompletionDebounceMs > 0 { + if other.CompletionDebounceMs > 0 { a.CompletionDebounceMs = other.CompletionDebounceMs } - if other.CompletionThrottleMs > 0 { + if other.CompletionThrottleMs > 0 { a.CompletionThrottleMs = other.CompletionThrottleMs } - if len(other.TriggerCharacters) > 0 { + if other.CompletionWaitAll != nil { + a.CompletionWaitAll = other.CompletionWaitAll + } + if len(other.TriggerCharacters) > 0 { a.TriggerCharacters = slices.Clone(other.TriggerCharacters) } - if s := strings.TrimSpace(other.InlineOpen); s != "" { + if s := strings.TrimSpace(other.InlineOpen); s != "" { a.InlineOpen = s } - if s := strings.TrimSpace(other.InlineClose); s != "" { + if s := strings.TrimSpace(other.InlineClose); s != "" { a.InlineClose = s } - if s := strings.TrimSpace(other.ChatSuffix); s != "" { + if s := strings.TrimSpace(other.ChatSuffix); s != "" { a.ChatSuffix = s } - if len(other.ChatPrefixes) > 0 { + if len(other.ChatPr