diff options
Diffstat (limited to 'docs/coverage.html')
| -rw-r--r-- | docs/coverage.html | 566 |
1 files changed, 284 insertions, 282 deletions
diff --git a/docs/coverage.html b/docs/coverage.html index 18886c9..356f77a 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -99,7 +99,7 @@ <option value="file21">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option> - <option value="file22">codeberg.org/snonux/hexai/internal/lsp/chat_commands.go (68.0%)</option> + <option value="file22">codeberg.org/snonux/hexai/internal/lsp/chat_commands.go (80.0%)</option> <option value="file23">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option> @@ -111,7 +111,7 @@ <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (88.8%)</option> - <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (87.6%)</option> + <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (89.7%)</option> <option value="file29">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> @@ -353,7 +353,7 @@ type CustomAction struct { } // Constructor: defaults for App (kept first among functions) -func newDefaultConfig() App <span class="cov6" title="45">{ +func newDefaultConfig() App <span class="cov6" title="47">{ // Coding-friendly default temperature across providers // Users can override per provider in config.toml (including 0.0). t := 0.2 @@ -409,7 +409,7 @@ func newDefaultConfig() App <span class="cov6" title="45">{ // Load reads configuration from a file and merges with defaults. // It respects the XDG Base Directory Specification. -func Load(logger *log.Logger) App <span class="cov6" title="42">{ return LoadWithOptions(logger, LoadOptions{}) }</span> +func Load(logger *log.Logger) App <span class="cov6" title="43">{ return LoadWithOptions(logger, LoadOptions{}) }</span> // LoadOptions tune how configuration is loaded at runtime. type LoadOptions struct { @@ -418,31 +418,31 @@ type LoadOptions struct { } // LoadWithOptions reads configuration and applies the requested loading options. -func LoadWithOptions(logger *log.Logger, opts LoadOptions) App <span class="cov6" title="44">{ +func LoadWithOptions(logger *log.Logger, opts LoadOptions) App <span class="cov6" title="46">{ cfg := newDefaultConfig() if logger == nil </span><span class="cov4" title="13">{ return cfg // Return defaults if no logger is provided (e.g. in tests) }</span> - <span class="cov5" title="31">configPath, err := getConfigPath() + <span class="cov5" title="33">configPath, err := getConfigPath() if err != nil </span><span class="cov0" title="0">{ logger.Printf("%v", err) // Even if config path cannot be resolved, keep defaults and optionally apply env overrides below. - }</span> else<span class="cov5" title="31"> { - if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov5" title="26">{ + }</span> else<span class="cov5" title="33"> { + if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov5" title="28">{ cfg.mergeWith(fileCfg) }</span> // When the config file is missing or invalid, we keep defaults and still // apply any environment overrides below (unless disabled). } - <span class="cov5" title="31">if !opts.IgnoreEnv </span><span class="cov5" title="29">{ + <span class="cov5" title="33">if !opts.IgnoreEnv </span><span class="cov5" title="30">{ // Environment overrides (take precedence over file) if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov3" title="7">{ cfg.mergeWith(envCfg) }</span> } - <span class="cov5" title="31">return cfg</span> + <span class="cov5" title="33">return cfg</span> } // Private helpers @@ -511,16 +511,16 @@ type sectionOpenAI struct { Presets map[string]string `toml:"presets"` } -func (s sectionOpenAI) isZero() bool <span class="cov5" title="26">{ +func (s sectionOpenAI) isZero() bool <span class="cov5" title="28">{ return strings.TrimSpace(s.Model) == "" && strings.TrimSpace(s.BaseURL) == "" && s.Temperature == nil && len(s.Presets) == 0 }</span> -func (s sectionOpenAI) resolvedModel() string <span class="cov2" title="4">{ +func (s sectionOpenAI) resolvedModel() string <span class="cov4" title="14">{ model := strings.TrimSpace(s.Model) if model == "" </span><span class="cov0" title="0">{ return "" }</span> - <span class="cov2" title="4">if len(s.Presets) == 0 </span><span class="cov2" title="3">{ + <span class="cov4" title="14">if len(s.Presets) == 0 </span><span class="cov4" title="13">{ return model }</span> <span class="cov1" title="1">if mapped := strings.TrimSpace(s.Presets[model]); mapped != "" </span><span class="cov1" title="1">{ @@ -609,11 +609,11 @@ type sectionTmux struct { CustomMenuHotkey string `toml:"custom_menu_hotkey"` } -func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ +func (fc *fileConfig) toApp() App <span class="cov5" title="28">{ out := App{} // Merge section: general - if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil </span><span class="cov3" title="7">{ + if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil </span><span class="cov3" title="9">{ tmp := App{ MaxTokens: fc.General.MaxTokens, ContextMode: fc.General.ContextMode, @@ -625,13 +625,13 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ }</span> // logging - <span class="cov5" title="26">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{ + <span class="cov5" title="28">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{ tmp := App{LogPreviewLimit: fc.Logging.LogPreviewLimit} out.mergeBasics(&tmp) }</span> // completion - <span class="cov5" title="26">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{ + <span class="cov5" title="28">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{ tmp := App{ CompletionDebounceMs: fc.Completion.CompletionDebounceMs, CompletionThrottleMs: fc.Completion.CompletionThrottleMs, @@ -641,31 +641,31 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ }</span> // triggers - <span class="cov5" title="26">if len(fc.Triggers.TriggerCharacters) > 0 </span><span class="cov2" title="3">{ + <span class="cov5" title="28">if len(fc.Triggers.TriggerCharacters) > 0 </span><span class="cov2" title="3">{ tmp := App{TriggerCharacters: fc.Triggers.TriggerCharacters} out.mergeBasics(&tmp) }</span> // inline - <span class="cov5" title="26">if (fc.Inline != sectionInline{}) </span><span class="cov1" title="1">{ + <span class="cov5" title="28">if (fc.Inline != sectionInline{}) </span><span class="cov1" title="1">{ tmp := App{InlineOpen: fc.Inline.InlineOpen, InlineClose: fc.Inline.InlineClose} out.mergeBasics(&tmp) }</span> // chat - <span class="cov5" title="26">if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) > 0 </span><span class="cov1" title="1">{ + <span class="cov5" title="28">if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) > 0 </span><span class="cov1" title="1">{ tmp := App{ChatSuffix: fc.Chat.ChatSuffix, ChatPrefixes: fc.Chat.ChatPrefixes} out.mergeBasics(&tmp) }</span> // provider - <span class="cov5" title="26">if strings.TrimSpace(fc.Provider.Name) != "" </span><span class="cov2" title="4">{ + <span class="cov5" title="28">if strings.TrimSpace(fc.Provider.Name) != "" </span><span class="cov2" title="4">{ tmp := App{Provider: fc.Provider.Name} out.mergeBasics(&tmp) }</span> // openai - <span class="cov5" title="26">if !fc.OpenAI.isZero() || fc.OpenAI.Temperature != nil </span><span class="cov2" title="4">{ + <span class="cov5" title="28">if !fc.OpenAI.isZero() || fc.OpenAI.Temperature != nil </span><span class="cov4" title="14">{ tmp := App{ OpenAIBaseURL: fc.OpenAI.BaseURL, OpenAIModel: fc.OpenAI.resolvedModel(), @@ -675,7 +675,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ }</span> // copilot - <span class="cov5" title="26">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{ + <span class="cov5" title="28">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{ tmp := App{ CopilotBaseURL: fc.Copilot.BaseURL, CopilotModel: fc.Copilot.Model, @@ -685,7 +685,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ }</span> // ollama - <span class="cov5" title="26">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{ + <span class="cov5" title="28">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{ tmp := App{ OllamaBaseURL: fc.Ollama.BaseURL, OllamaModel: fc.Ollama.Model, @@ -696,7 +696,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ // prompts // completion - <span class="cov5" title="26">if (fc.Prompts.Completion != sectionPromptsCompletion{}) </span><span class="cov1" title="1">{ + <span class="cov5" title="28">if (fc.Prompts.Completion != sectionPromptsCompletion{}) </span><span class="cov1" title="1">{ if strings.TrimSpace(fc.Prompts.Completion.SystemGeneral) != "" </span><span class="cov1" title="1">{ out.PromptCompletionSystemGeneral = fc.Prompts.Completion.SystemGeneral }</span> @@ -717,11 +717,11 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ }</span> } // chat - <span class="cov5" title="26">if strings.TrimSpace(fc.Prompts.Chat.System) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="28">if strings.TrimSpace(fc.Prompts.Chat.System) != "" </span><span class="cov1" title="1">{ out.PromptChatSystem = fc.Prompts.Chat.System }</span> // code action - <span class="cov5" title="26">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" || + <span class="cov5" title="28">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) != "" || @@ -778,7 +778,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ } } // cli - <span class="cov5" title="26">if (fc.Prompts.CLI != sectionPromptsCLI{}) </span><span class="cov1" title="1">{ + <span class="cov5" title="28">if (fc.Prompts.CLI != sectionPromptsCLI{}) </span><span class="cov1" title="1">{ if strings.TrimSpace(fc.Prompts.CLI.DefaultSystem) != "" </span><span class="cov1" title="1">{ out.PromptCLIDefaultSystem = fc.Prompts.CLI.DefaultSystem }</span> @@ -787,24 +787,24 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="26">{ }</span> } // provider-native - <span class="cov5" title="26">if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="28">if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" </span><span class="cov1" title="1">{ out.PromptNativeCompletion = fc.Prompts.ProviderNative.Completion }</span> // tmux - <span class="cov5" title="26">if (fc.Tmux != sectionTmux{}) </span><span class="cov2" title="3">{ + <span class="cov5" title="28">if (fc.Tmux != sectionTmux{}) </span><span class="cov2" title="3">{ out.TmuxCustomMenuHotkey = strings.TrimSpace(fc.Tmux.CustomMenuHotkey) }</span> // stats - <span class="cov5" title="26">if fc.Stats.WindowMinutes > 0 </span><span class="cov0" title="0">{ + <span class="cov5" title="28">if fc.Stats.WindowMinutes > 0 </span><span class="cov0" title="0">{ out.StatsWindowMinutes = fc.Stats.WindowMinutes }</span> - <span class="cov5" title="26">return out</span> + <span class="cov5" title="28">return out</span> } -func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="32">{ +func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="34">{ b, err := os.ReadFile(path) if err != nil </span><span class="cov2" title="4">{ if !os.IsNotExist(err) && logger != nil </span><span class="cov0" title="0">{ @@ -813,7 +813,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co <span class="cov2" title="4">return nil, err</span> } - <span class="cov5" title="28">var tables fileConfig + <span class="cov5" title="30">var tables fileConfig errTables := toml.NewDecoder(strings.NewReader(string(b))).Decode(&tables) // Raw map for validation/presence checks var raw map[string]any @@ -826,7 +826,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co } // Reject legacy flat keys at top-level (sectioned-only config is allowed) - <span class="cov5" title="26">legacy := map[string]struct{}{ + <span class="cov5" title="28">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": {}, @@ -835,8 +835,8 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co "ollama_model": {}, "ollama_base_url": {}, "ollama_temperature": {}, "copilot_model": {}, "copilot_base_url": {}, "copilot_temperature": {}, } - for k := range raw </span><span class="cov6" title="52">{ - if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable </span><span class="cov6" title="49">{ + for k := range raw </span><span class="cov6" title="64">{ + if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable </span><span class="cov6" title="61">{ continue</span> } <span class="cov2" title="3">if _, isLegacy := legacy[k]; isLegacy </span><span class="cov0" title="0">{ @@ -844,13 +844,13 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co }</span> } - <span class="cov5" title="26">if logger != nil </span><span class="cov5" title="26">{ + <span class="cov5" title="28">if logger != nil </span><span class="cov5" title="28">{ logger.Printf("loaded configuration from %s (TOML)", path) }</span> // Merge order: flat first, then tables (so tables win over zero flat values) // Build App from tables only - <span class="cov5" title="26">tab := tables.toApp() + <span class="cov5" title="28">tab := tables.toApp() // Ensure explicit values from raw map are respected (defensive for ints) if t, ok := raw["completion"].(map[string]any); ok </span><span class="cov2" title="3">{ if v, present := t["manual_invoke_min_prefix"]; present </span><span class="cov2" title="3">{ @@ -864,7 +864,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co } } } - <span class="cov5" title="26">if t, ok := raw["logging"].(map[string]any); ok </span><span class="cov2" title="3">{ + <span class="cov5" title="28">if t, ok := raw["logging"].(map[string]any); ok </span><span class="cov2" title="3">{ if v, present := t["log_preview_limit"]; present </span><span class="cov2" title="3">{ switch vv := v.(type) </span>{ case int64:<span class="cov2" title="3"> @@ -876,142 +876,142 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co } } } - <span class="cov5" title="26">return &tab, nil</span> + <span class="cov5" title="28">return &tab, nil</span> } -func (a *App) mergeWith(other *App) <span class="cov5" title="33">{ +func (a *App) mergeWith(other *App) <span class="cov5" title="35">{ a.mergeBasics(other) a.mergeProviderFields(other) a.mergePrompts(other) }</span> // mergeBasics merges general (non-provider) fields. -func (a *App) mergeBasics(other *App) <span class="cov6" title="53">{ - if other.MaxTokens > 0 </span><span class="cov4" title="17">{ +func (a *App) mergeBasics(other *App) <span class="cov6" title="57">{ + if other.MaxTokens > 0 </span><span class="cov5" title="21">{ a.MaxTokens = other.MaxTokens }</span> - <span class="cov6" title="53">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov3" title="7">{ + <span class="cov6" title="57">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov3" title="7">{ a.ContextMode = s }</span> - <span class="cov6" title="53">if other.ContextWindowLines > 0 </span><span class="cov3" title="7">{ + <span class="cov6" title="57">if other.ContextWindowLines > 0 </span><span class="cov3" title="7">{ a.ContextWindowLines = other.ContextWindowLines }</span> - <span class="cov6" title="53">if other.MaxContextTokens > 0 </span><span class="cov3" title="7">{ + <span class="cov6" title="57">if other.MaxContextTokens > 0 </span><span class="cov3" title="7">{ a.MaxContextTokens = other.MaxContextTokens }</span> - <span class="cov6" title="53">if other.LogPreviewLimit >= 0 </span><span class="cov6" title="53">{ + <span class="cov6" title="57">if other.LogPreviewLimit >= 0 </span><span class="cov6" title="57">{ a.LogPreviewLimit = other.LogPreviewLimit }</span> - <span class="cov6" title="53">if other.CodingTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0 + <span class="cov6" title="57">if other.CodingTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0 a.CodingTemperature = other.CodingTemperature }</span> - <span class="cov6" title="53">if other.ManualInvokeMinPrefix >= 0 </span><span class="cov6" title="53">{ + <span class="cov6" title="57">if other.ManualInvokeMinPrefix >= 0 </span><span class="cov6" title="57">{ a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix }</span> - <span class="cov6" title="53">if other.CompletionDebounceMs > 0 </span><span class="cov3" title="7">{ + <span class="cov6" title="57">if other.CompletionDebounceMs > 0 </span><span class="cov3" title="7">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span> - <span class="cov6" title="53">if other.CompletionThrottleMs > 0 </span><span class="cov3" title="7">{ + <span class="cov6" title="57">if other.CompletionThrottleMs > 0 </span><span class="cov3" title="7">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span> - <span class="cov6" title="53">if len(other.TriggerCharacters) > 0 </span><span class="cov3" title="7">{ + <span class="cov6" title="57">if len(other.TriggerCharacters) > 0 </span><span class="cov3" title="7">{ a.TriggerCharacters = slices.Clone(other.TriggerCharacters) }</span> - <span class="cov6" title="53">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov1" title="2">{ + <span class="cov6" title="57">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov1" title="2">{ a.InlineOpen = s }</span> - <span class="cov6" title="53">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov1" title="2">{ + <span class="cov6" title="57">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov1" title="2">{ a.InlineClose = s }</span> - <span class="cov6" title="53">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov1" title="2">{ + <span class="cov6" title="57">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov1" title="2">{ a.ChatSuffix = s }</span> - <span class="cov6" title="53">if len(other.ChatPrefixes) > 0 </span><span class="cov1" title="2">{ + <span class="cov6" title="57">if len(other.ChatPrefixes) > 0 </span><span class="cov1" title="2">{ a.ChatPrefixes = slices.Clone(other.ChatPrefixes) }</span> - <span class="cov6" title="53">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov4" title="13">{ + <span class="cov6" title="57">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov4" title="13">{ a.Provider = s }</span> } // mergePrompts copies non-empty prompt templates from other. -func (a *App) mergePrompts(other *App) <span class="cov5" title="33">{ +func (a *App) mergePrompts(other *App) <span class="cov5" title="35">{ // Completion if strings.TrimSpace(other.PromptCompletionSystemGeneral) != "" </span><span class="cov1" title="1">{ a.PromptCompletionSystemGeneral = other.PromptCompletionSystemGeneral }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionSystemParams) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCompletionSystemParams) != "" </span><span class="cov1" title="1">{ a.PromptCompletionSystemParams = other.PromptCompletionSystemParams }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionSystemInline) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCompletionSystemInline) != "" </span><span class="cov1" title="1">{ a.PromptCompletionSystemInline = other.PromptCompletionSystemInline }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionUserGeneral) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCompletionUserGeneral) != "" </span><span class="cov1" title="1">{ a.PromptCompletionUserGeneral = other.PromptCompletionUserGeneral }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionUserParams) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCompletionUserParams) != "" </span><span class="cov1" title="1">{ a.PromptCompletionUserParams = other.PromptCompletionUserParams }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionExtraHeader) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCompletionExtraHeader) != "" </span><span class="cov1" title="1">{ a.PromptCompletionExtraHeader = other.PromptCompletionExtraHeader }</span> // Provider-native - <span class="cov5" title="33">if strings.TrimSpace(other.PromptNativeCompletion) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptNativeCompletion) != "" </span><span class="cov1" title="1">{ a.PromptNativeCompletion = other.PromptNativeCompletion }</span> // Chat - <span class="cov5" title="33">if strings.TrimSpace(other.PromptChatSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptChatSystem) != "" </span><span class="cov1" title="1">{ a.PromptChatSystem = other.PromptChatSystem }</span> // Code actions - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionRewriteSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionRewriteSystem) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionRewriteSystem = other.PromptCodeActionRewriteSystem }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionDiagnosticsSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionDiagnosticsSystem) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionDiagnosticsSystem = other.PromptCodeActionDiagnosticsSystem }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionDocumentSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionDocumentSystem) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionDocumentSystem = other.PromptCodeActionDocumentSystem }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionRewriteUser) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionRewriteUser) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionRewriteUser = other.PromptCodeActionRewriteUser }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionDiagnosticsUser) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionDiagnosticsUser) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionDiagnosticsUser = other.PromptCodeActionDiagnosticsUser }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionDocumentUser) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionDocumentUser) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionDocumentUser = other.PromptCodeActionDocumentUser }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionGoTestSystem = other.PromptCodeActionGoTestSystem }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionGoTestUser = other.PromptCodeActionGoTestUser }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionSimplifySystem) != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionSimplifySystem) != "" </span><span class="cov0" title="0">{ a.PromptCodeActionSimplifySystem = other.PromptCodeActionSimplifySystem }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionSimplifyUser) != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCodeActionSimplifyUser) != "" </span><span class="cov0" title="0">{ a.PromptCodeActionSimplifyUser = other.PromptCodeActionSimplifyUser }</span> // CLI - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" </span><span class="cov1" title="1">{ a.PromptCLIDefaultSystem = other.PromptCLIDefaultSystem }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.PromptCLIExplainSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="35">if strings.TrimSpace(other.PromptCLIExplainSystem) != "" </span><span class="cov1" title="1">{ a.PromptCLIExplainSystem = other.PromptCLIExplainSystem }</span> // Custom actions - <span class="cov5" title="33">if len(other.CustomActions) > 0 </span><span class="cov4" title="16">{ + <span class="cov5" title="35">if len(other.CustomActions) > 0 </span><span class="cov4" title="16">{ a.CustomActions = append([]CustomAction{}, other.CustomActions...) }</span> - <span class="cov5" title="33">if strings.TrimSpace(other.TmuxCustomMenuHotkey) != "" </span><span class="cov2" title="3">{ + <span class="cov5" title="35">if strings.TrimSpace(other.TmuxCustomMenuHotkey) != "" </span><span class="cov2" title="3">{ a.TmuxCustomMenuHotkey = other.TmuxCustomMenuHotkey }</span> } // Validate checks custom actions and tmux settings for duplicates and consistency. -func (a App) Validate() error <span class="cov5" title="22">{ +func (a App) Validate() error <span class="cov5" title="23">{ // Normalize and check duplicates for IDs and hotkeys seenID := make(map[string]struct{}) seenHK := make(map[string]struct{}) @@ -1054,7 +1054,7 @@ func (a App) Validate() error <span class="cov5" title="22">{ } } // Tmux custom menu hotkey validation - <span class="cov4" title="17">if hk := strings.TrimSpace(a.TmuxCustomMenuHotkey); hk != "" </span><span class="cov1" title="2">{ + <span class="cov4" title="18">if hk := strings.TrimSpace(a.TmuxCustomMenuHotkey); hk != "" </span><span class="cov1" title="2">{ if len([]rune(hk)) != 1 </span><span class="cov0" title="0">{ return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s", hk) }</span> @@ -1064,43 +1064,43 @@ func (a App) Validate() error <span class="cov5" title="22">{ return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s (clashes with built-in)", hk)</span> } } - <span class="cov4" title="16">return nil</span> + <span class="cov4" title="17">return nil</span> } // mergeProviderFields merges per-provider configuration. -func (a *App) mergeProviderFields(other *App) <span class="cov6" title="43">{ - if s := strings.TrimSpace(other.OpenAIBaseURL); s != "" </span><span class="cov3" title="7">{ +func (a *App) mergeProviderFields(other *App) <span class="cov6" title="55">{ + if s := strings.TrimSpace(other.OpenAIBaseURL); s != "" </span><span class="cov5" title="27">{ a.OpenAIBaseURL = s }</span> - <span class="cov6" title="43">if s := strings.TrimSpace(other.OpenAIModel); s != "" </span><span class="cov4" title="13">{ + <span class="cov6" title="55">if s := strings.TrimSpace(other.OpenAIModel); s != "" </span><span class="cov5" title="33">{ a.OpenAIModel = s }</span> - <span class="cov6" title="43">if other.OpenAITemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0 + <span class="cov6" title="55">if other.OpenAITemperature != nil </span><span class="cov5" title="27">{ // allow explicit 0.0 a.OpenAITemperature = other.OpenAITemperature }</span> - <span class="cov6" title="43">if s := strings.TrimSpace(other.OllamaBaseURL); s != "" </span><span class="cov3" title="7">{ + <span class="cov6" title="55">if s := strings.TrimSpace(other.OllamaBaseURL); s != "" </span><span class="cov3" title="7">{ a.OllamaBaseURL = s }</span> - <span class="cov6" title="43">if s := strings.TrimSpace(other.OllamaModel); s != "" </span><span class="cov3" title="7">{ + <span class="cov6" title="55">if s := strings.TrimSpace(other.OllamaModel); s != "" </span><span class="cov3" title="7">{ a.OllamaModel = s }</span> - <span class="cov6" title="43">if other.OllamaTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0 + <span class="cov6" title="55">if other.OllamaTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0 a.OllamaTemperature = other.OllamaTemperature }</span> - <span class="cov6" title="43">if s := strings.TrimSpace(other.CopilotBaseURL); s != "" </span><span class="cov3" title="7">{ + <span class="cov6" title="55">if s := strings.TrimSpace(other.CopilotBaseURL); s != "" </span><span class="cov3" title="7">{ a.CopilotBaseURL = s }</span> - <span class="cov6" title="43">if s := strings.TrimSpace(other.CopilotModel); s != "" </span><span class="cov3" title="7">{ + <span class="cov6" title="55">if s := strings.TrimSpace(other.CopilotModel); s != "" </span><span class="cov3" title="7">{ a.CopilotModel = s }</span> - <span class="cov6" title="43">if other.CopilotTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0 + <span class="cov6" title="55">if other.CopilotTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0 a.CopilotTemperature = other.CopilotTemperature }</span> } -func getConfigPath() (string, error) <span class="cov5" title="32">{ +func getConfigPath() (string, error) <span class="cov5" title="34">{ var configPath string - if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov5" title="22">{ + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov5" title="24">{ configPath = filepath.Join(xdgConfigHome, "hexai", "config.toml") }</span> else<span class="cov4" title="10"> { home, err := os.UserHomeDir() @@ -1109,22 +1109,22 @@ func getConfigPath() (string, error) <span class="cov5" title="32">{ }</span> <span class="cov4" title="10">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span> } - <span class="cov5" title="32">return configPath, nil</span> + <span class="cov5" title="34">return configPath, nil</span> } // --- Environment overrides --- // loadFromEnv constructs an App containing only fields set via HEXAI_* env vars. // These values should take precedence over file config when merged. -func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="29">{ +func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="30">{ var out App var any bool // helpers - getenv := func(k string) string </span><span class="cov10" title="754">{ return strings.TrimSpace(os.Getenv(k)) }</span> - <span class="cov5" title="29">parseInt := func(k string) (int, bool) </span><span class="cov8" title="203">{ + getenv := func(k string) string </span><span class="cov10" title="780">{ return strings.TrimSpace(os.Getenv(k)) }</span> + <span class="cov5" title="30">parseInt := func(k string) (int, bool) </span><span class="cov8" title="210">{ v := getenv(k) - if v == "" </span><span class="cov8" title="194">{ + if v == "" </span><span class="cov8" title="201">{ return 0, false }</span> <span class="cov3" title="9">n, err := strconv.Atoi(v) @@ -1136,9 +1136,9 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="29">{ } <span class="cov3" title="9">return n, true</span> } - <span class="cov5" title="29">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="116">{ + <span class="cov5" title="30">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="120">{ v := getenv(k) - if v == "" </span><span class="cov7" title="112">{ + if v == "" </span><span class="cov7" title="116">{ return nil, false }</span> <span class="cov2" title="4">f, err := strconv.ParseFloat(v, 64) @@ -1151,43 +1151,43 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="29">{ <span class="cov2" title="4">return &f, true</span> } - <span class="cov5" title="29">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov2" title="3">{ + <span class="cov5" title="30">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov2" title="3">{ out.MaxTokens = n any = true }</span> - <span class="cov5" title="29">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ out.ContextMode = s any = true }</span> - <span class="cov5" title="29">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ out.ContextWindowLines = n any = true }</span> - <span class="cov5" title="29">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ out.MaxContextTokens = n any = true }</span> - <span class="cov5" title="29">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ out.LogPreviewLimit = n any = true }</span> - <span class="cov5" title="29">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ out.ManualInvokeMinPrefix = n any = true }</span> - <span class="cov5" title="29">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ out.CompletionDebounceMs = n any = true }</span> - <span class="cov5" title="29">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ out.CompletionThrottleMs = n any = true }</span> - <span class="cov5" title="29">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CodingTemperature = f any = true }</span> - <span class="cov5" title="29">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ parts := strings.Split(s, ",") out.TriggerCharacters = nil for _, p := range parts </span><span class="cov2" title="3">{ @@ -1197,19 +1197,19 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="29">{ } <span class="cov1" title="1">any = true</span> } - <span class="cov5" title="29">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="30">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ out.InlineOpen = s any = true }</span> - <span class="cov5" title="29">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="30">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ out.InlineClose = s any = true }</span> - <span class="cov5" title="29">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="30">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ out.ChatSuffix = s any = true }</span> - <span class="cov5" title="29">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="30">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ parts := strings.Split(s, ",") out.ChatPrefixes = nil for _, p := range parts </span><span class="cov0" title="0">{ @@ -1219,17 +1219,17 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="29">{ } <span class="cov0" title="0">any = true</span> } - <span class="cov5" title="29">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov3" title="5">{ + <span class="cov5" title="30">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov3" title="5">{ out.Provider = s any = true }</span> - <span class="cov5" title="29">modelForce := strings.TrimSpace(getenv("HEXAI_MODEL_FORCE")) + <span class="cov5" title="30">modelForce := strings.TrimSpace(getenv("HEXAI_MODEL_FORCE")) modelGeneric := strings.TrimSpace(getenv("HEXAI_MODEL")) providerLower := strings.ToLower(strings.TrimSpace(out.Provider)) forceUsed := false genericUsed := false - pickModel := func(providerName, specific string) (string, bool) </span><span class="cov7" title="87">{ + pickModel := func(providerName, specific string) (string, bool) </span><span class="cov7" title="90">{ specific = strings.TrimSpace(specific) nameLower := strings.ToLower(strings.TrimSpace(providerName)) if modelForce != "" </span><span class="cov2" title="3">{ @@ -1242,10 +1242,10 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="29">{ return modelForce, true }</span> } - <span class="cov7" title="86">if specific != "" </span><span class="cov2" title="4">{ + <span class="cov7" title="89">if specific != "" </span><span class="cov2" title="4">{ return specific, true }</span> - <span class="cov6" title="82">if modelGeneric != "" </span><span class="cov3" title="8">{ + <span class="cov7" title="85">if modelGeneric != "" </span><span class="cov3" title="8">{ if providerLower == nameLower </span><span class="cov1" title="2">{ return modelGeneric, true }</span> @@ -1254,50 +1254,50 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="29">{ return modelGeneric, true }</span> } - <span class="cov6" title="80">return "", false</span> + <span class="cov6" title="83">return "", false</span> } // Provider-specific - <span class="cov5" title="29">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIBaseURL = s any = true }</span> - <span class="cov5" title="29">if model, ok := pickModel("openai", getenv("HEXAI_OPENAI_MODEL")); ok </span><span class="cov3" title="5">{ + <span class="cov5" title="30">if model, ok := pickModel("openai", getenv("HEXAI_OPENAI_MODEL")); ok </span><span class="cov3" title="5">{ out.OpenAIModel = model any = true }</span> - <span class="cov5" title="29">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OpenAITemperature = f any = true }</span> - <span class="cov5" title="29">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OllamaBaseURL = s any = true }</span> - <span class="cov5" title="29">if model, ok := pickModel("ollama", getenv("HEXAI_OLLAMA_MODEL")); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if model, ok := pickModel("ollama", getenv("HEXAI_OLLAMA_MODEL")); ok </span><span class="cov1" title="1">{ out.OllamaModel = model any = true }</span> - <span class="cov5" title="29">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OllamaTemperature = f any = true }</span> - <span class="cov5" title="29">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.CopilotBaseURL = s any = true }</span> - <span class="cov5" title="29">if model, ok := pickModel("copilot", getenv("HEXAI_COPILOT_MODEL")); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if model, ok := pickModel("copilot", getenv("HEXAI_COPILOT_MODEL")); ok </span><span class="cov1" title="1">{ out.CopilotModel = model any = true }</span> - <span class="cov5" title="29">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="30">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CopilotTemperature = f any = true }</span> - <span class="cov5" title="29">if !any </span><span class="cov5" title="22">{ + <span class="cov5" title="30">if !any </span><span class="cov5" title="23">{ return nil }</span> <span class="cov3" title="7">return &out</span> @@ -3814,14 +3814,14 @@ type chatCommandResult struct { message string } -func (s *Server) chatCommandResponse(uri string, lineIdx int, prompt string) (chatCommandResult, bool) <span class="cov10" title="8">{ +func (s *Server) chatCommandResponse(uri string, lineIdx int, prompt string) (chatCommandResult, bool) <span class="cov10" title="9">{ trimmed := strings.TrimSpace(s.stripTrailingTrigger(prompt)) - if trimmed == "" || !strings.HasPrefix(trimmed, "/") </span><span class="cov10" title="8">{ + if trimmed == "" || !strings.HasPrefix(trimmed, "/") </span><span class="cov9" title="8">{ return chatCommandResult{}, false }</span> - <span class="cov0" title="0">switch </span>{ - case strings.HasPrefix(trimmed, "/reload"):<span class="cov0" title="0"> + <span class="cov1" title="1">switch </span>{ + case strings.HasPrefix(trimmed, "/reload"):<span class="cov1" title="1"> return s.handleReloadCommand(), true</span> case strings.HasPrefix(trimmed, "/help"):<span class="cov0" title="0"> return s.handleHelpCommand(), true</span> @@ -3838,30 +3838,30 @@ func (s *Server) handleHelpCommand() chatCommandResult <span class="cov1" title= return chatCommandResult{message: strings.Join(lines, "\n")} }</span> -func (s *Server) handleReloadCommand() chatCommandResult <span class="cov1" title="1">{ +func (s *Server) handleReloadCommand() chatCommandResult <span class="cov3" title="2">{ if s.configStore == nil </span><span class="cov0" title="0">{ return chatCommandResult{message: "Reload unavailable: no config store"} }</span> - <span class="cov1" title="1">changes, err := s.configStore.Reload(s.logger, appconfig.LoadOptions{IgnoreEnv: true}) + <span class="cov3" title="2">changes, err := s.configStore.Reload(s.logger, appconfig.LoadOptions{IgnoreEnv: true}) if err != nil </span><span class="cov0" title="0">{ s.logger.Printf("config reload failed: %v", err) return chatCommandResult{message: fmt.Sprintf("Reload failed: %v", err)} }</span> - <span class="cov1" title="1">summary := formatReloadSummary(changes) + <span class="cov3" title="2">summary := formatReloadSummary(changes) s.logger.Print(summary) return chatCommandResult{message: summary}</span> } -func formatReloadSummary(changes []runtimeconfig.Change) string <span class="cov4" title="2">{ - if len(changes) == 0 </span><span class="cov0" title="0">{ +func formatReloadSummary(changes []runtimeconfig.Change) string <span class="cov5" title="3">{ + if len(changes) == 0 </span><span class="cov1" title="1">{ return "Reloaded config (no changes detected)." }</span> - <span class="cov4" title="2">lines := make([]string, 0, len(changes)+1) + <span class="cov3" title="2">lines := make([]string, 0, len(changes)+1) lines = append(lines, fmt.Sprintf("Reloaded config (%d changes):", len(changes))) for _, ch := range changes </span><span class="cov5" title="3">{ lines = append(lines, fmt.Sprintf("- %s: %s → %s", ch.Key, ch.Old, ch.New)) }</span> - <span class="cov4" title="2">return strings.Join(lines, "\n")</span> + <span class="cov3" title="2">return strings.Join(lines, "\n")</span> } </pre> @@ -3965,7 +3965,7 @@ type document struct { lines []string } -func (s *Server) setDocument(uri, text string) <span class="cov8" title="39">{ +func (s *Server) setDocument(uri, text string) <span class="cov8" title="40">{ s.mu.Lock() defer s.mu.Unlock() s.docs[uri] = &document{uri: uri, text: text, lines: splitLines(text)} @@ -3983,14 +3983,14 @@ func (s *Server) markActivity() <span class="cov3" title="4">{ s.mu.Unlock() }</span> -func (s *Server) getDocument(uri string) *document <span class="cov10" title="85">{ +func (s *Server) getDocument(uri string) *document <span class="cov10" title="87">{ s.mu.RLock() defer s.mu.RUnlock() return s.docs[uri] }</span> // splitLines splits the input string into lines, normalizing line endings to '\n'. -func splitLines(sx string) []string <span class="cov8" title="51">{ +func splitLines(sx string) []string <span class="cov8" title="52">{ sx = strings.ReplaceAll(sx, "\r\n", "\n") return strings.Split(sx, "\n") }</span> @@ -5656,42 +5656,42 @@ func (s *Server) handleDidClose(req Request) <span class="cov1" title="1">{ // docBeforeAfter returns the full document text split at the given position. // The returned strings are the text before the cursor (inclusive of anything // left of the position) and the text after the cursor. -func (s *Server) docBeforeAfter(uri string, pos Position) (string, string) <span class="cov7" title="8">{ +func (s *Server) docBeforeAfter(uri string, pos Position) (string, string) <span class="cov6" title="8">{ d := s.getDocument(uri) - if d == nil </span><span class="cov5" title="4">{ + if d == nil </span><span class="cov4" title="4">{ return "", "" }</span> // Clamp indices - <span class="cov5" title="4">line := pos.Line + <span class="cov4" title="4">line := pos.Line if line < 0 </span><span class="cov0" title="0">{ line = 0 }</span> - <span class="cov5" title="4">if line >= len(d.lines) </span><span class="cov1" title="1">{ + <span class="cov4" title="4">if line >= len(d.lines) </span><span class="cov1" title="1">{ line = len(d.lines) - 1 }</span> - <span class="cov5" title="4">col := pos.Character + <span class="cov4" title="4">col := pos.Character if col < 0 </span><span class="cov0" title="0">{ col = 0 }</span> - <span class="cov5" title="4">if col > len(d.lines[line]) </span><span class="cov1" title="1">{ + <span class="cov4" title="4">if col > len(d.lines[line]) </span><span class="cov1" title="1">{ col = len(d.lines[line]) }</span> // Build before - <span class="cov5" title="4">var b strings.Builder + <span class="cov4" title="4">var b strings.Builder for i := 0; i < line; i++ </span><span class="cov5" title="5">{ b.WriteString(d.lines[i]) b.WriteByte('\n') }</span> - <span class="cov5" title="4">b.WriteString(d.lines[line][:col]) + <span class="cov4" title="4">b.WriteString(d.lines[line][:col]) before := b.String() // Build after var a strings.Builder a.WriteString(d.lines[line][col:]) - for i := line + 1; i < len(d.lines); i++ </span><span class="cov5" title="4">{ + for i := line + 1; i < len(d.lines); i++ </span><span class="cov4" title="4">{ a.WriteByte('\n') a.WriteString(d.lines[i]) }</span> - <span class="cov5" title="4">return before, a.String()</span> + <span class="cov4" title="4">return before, a.String()</span> } // --- in-editor chat (";C ...") --- @@ -5699,76 +5699,78 @@ func (s *Server) docBeforeAfter(uri string, pos Position) (string, string) <span // detectAndHandleChat scans the current document for any line that starts with // a new trigger pair (e.g., "?>" ",>" ":>" ";>") at EOL and inserts the LLM // reply below. -func (s *Server) detectAndHandleChat(uri string) <span class="cov7" title="10">{ +func (s *Server) detectAndHandleChat(uri string) <span class="cov7" title="11">{ d := s.getDocument(uri) if d == nil || len(d.lines) == 0 </span><span class="cov0" title="0">{ return }</span> - <span class="cov7" title="10">suffix, prefixes, _ := s.chatConfig() - for i, raw := range d.lines </span><span class="cov10" title="22">{ + <span class="cov7" title="11">suffix, prefixes, _ := s.chatConfig() + for i, raw := range d.lines </span><span class="cov10" title="23">{ // Find last non-space character index j := len(raw) - 1 - for j >= 0 </span><span class="cov9" title="19">{ + for j >= 0 </span><span class="cov9" title="20">{ if raw[j] == ' ' || raw[j] == '\t' </span><span class="cov0" title="0">{ j-- continue</span> } - <span class="cov9" title="19">break</span> + <span class="cov9" title="20">break</span> } - <span class="cov10" title="22">if j < 0 </span><span class="cov4" title="3">{ + <span class="cov10" title="23">if j < 0 </span><span class="cov4" title="3">{ continue</span> } - // Check suffix/prefix according to configuration - <span class="cov9" title="19">if suffix == "" </span><span class="cov0" title="0">{ + // Check suffix and derive the prompt text before validating prefixes + <span class="cov9" title="20">if suffix == "" </span><span class="cov0" title="0">{ continue</span> } - // Last non-space must equal suffix - <span class="cov9" title="19">if string(raw[j]) != suffix </span><span class="cov7" title="10">{ + <span class="cov9" title="20">if string(raw[j]) != suffix </span><span class="cov7" title="10">{ continue</span> } - // Require at least one char before suffix and that char must be in chatPrefixes - <span class="cov7" title="9">if j < 1 </span><span class="cov0" title="0">{ + <span class="cov7" title="10">removeCount := len(suffix) + base := raw[:j+1-removeCount] + prompt := strings.TrimSpace(base) + if prompt == "" </span><span class="cov0" title="0">{ continue</span> } - <span class="cov7" title="9">prev := string(raw[j-1]) - isTrigger := false - for _, pfx := range prefixes </span><span class="cov7" title="9">{ - if prev == pfx </span><span class="cov7" title="9">{ - isTrigger = true - break</span> + // Slash commands (`/foo>`) do not require a prefix trigger. + <span class="cov7" title="10">isCommand := strings.HasPrefix(prompt, "/") + if !isCommand </span><span class="cov7" title="9">{ + // Require at least one char before suffix and that char must be in chatPrefixes + if j < 1 </span><span class="cov0" title="0">{ + continue</span> + } + <span class="cov7" title="9">prev := string(raw[j-1]) + match := false + for _, pfx := range prefixes </span><span class="cov7" title="9">{ + if prev == pfx </span><span class="cov7" title="9">{ + match = true + break</span> + } + } + <span class="cov7" title="9">if !match </span><span class="cov0" title="0">{ + continue</span> } - } - <span class="cov7" title="9">if !isTrigger </span><span class="cov0" title="0">{ - continue</span> } // Avoid double-answering: if the next non-empty line starts with '>' we skip. - <span class="cov7" title="9">k := i + 1 - for k < len(d.lines) && strings.TrimSpace(d.lines[k]) == "" </span><span class="cov7" title="10">{ + <span class="cov7" title="10">k := i + 1 + for k < len(d.lines) && strings.TrimSpace(d.lines[k]) == "" </span><span class="cov7" title="11">{ k++ }</span> - <span class="cov7" title="9">if k < len(d.lines) && strings.HasPrefix(strings.TrimSpace(d.lines[k]), ">") </span><span class="cov1" title="1">{ - continue</span> - } - // Derive prompt by removing only the trailing '>' - <span class="cov7" title="8">removeCount := len(suffix) - base := raw[:j+1-removeCount] - prompt := strings.TrimSpace(base) - if prompt == "" </span><span class="cov0" title="0">{ + <span class="cov7" title="10">if k < len(d.lines) && strings.HasPrefix(strings.TrimSpace(d.lines[k]), ">") </span><span class="cov1" title="1">{ continue</span> } - <span class="cov7" title="8">lineIdx := i + <span class="cov7" title="9">lineIdx := i lastIdx := j - if resp, ok := s.chatCommandResponse(uri, lineIdx, prompt); ok </span><span class="cov0" title="0">{ + if resp, ok := s.chatCommandResponse(uri, lineIdx, prompt); ok </span><span class="cov1" title="1">{ msg := strings.TrimSpace(resp.message) - if msg != "" </span><span class="cov0" title="0">{ + if msg != "" </span><span class="cov1" title="1">{ s.applyChatEdits(uri, lineIdx, lastIdx, removeCount, "> "+msg) }</span> - <span class="cov0" title="0">return</span> + <span class="cov1" title="1">return</span> } - <span class="cov7" title="8">if s.currentLLMClient() == nil </span><span class="cov0" title="0">{ + <span class="cov6" title="8">if s.currentLLMClient() == nil </span><span class="cov0" title="0">{ continue</span> } - <span class="cov7" title="8">go func(prompt string, remove int) </span><span class="cov7" title="8">{ + <span class="cov6" title="8">go func(prompt string, remove int) </span><span class="cov6" title="8">{ ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) defer cancel() // Build messages with history and context_mode aware extras. @@ -5779,32 +5781,32 @@ func (s *Server) detectAndHandleChat(uri string) <span class="cov7" title="10">{ if client == nil </span><span class="cov0" title="0">{ return }</span> - <span class="cov7" title="8">logging.Logf("lsp ", "chat llm=requesting model=%s", client.DefaultModel()) + <span class="cov6" title="8">logging.Logf("lsp ", "chat llm=requesting model=%s", client.DefaultModel()) text, err := s.chatWithStats(ctx, msgs, opts...) if err != nil </span><span class="cov0" title="0">{ logging.Logf("lsp ", "chat llm error: %v", err) return }</span> - <span class="cov7" title="8">out := strings.TrimSpace(stripCodeFences(text)) + <span class="cov6" title="8">out := strings.TrimSpace(stripCodeFences(text)) if out == "" </span><span class="cov0" title="0">{ return }</span> - <span class="cov7" title="8">s.applyChatEdits(uri, lineIdx, lastIdx, remove, "> "+out)</span> + <span class="cov6" title="8">s.applyChatEdits(uri, lineIdx, lastIdx, remove, "> "+out)</span> }(prompt, removeCount) // Only handle one per change tick to avoid flooding - <span class="cov7" title="8">break</span> + <span class="cov6" title="8">break</span> } } // applyChatEdits removes the triggering punctuation at end of the line and // inserts two newlines followed by a new line with the response prefixed. -func (s *Server) applyChatEdits(uri string, lineIdx int, lastNonSpace int, removeCount int, response string) <span class="cov7" title="8">{ +func (s *Server) applyChatEdits(uri string, lineIdx int, lastNonSpace int, removeCount int, response string) <span class="cov7" title="9">{ d := s.getDocument(uri) if d == nil </span><span class="cov0" title="0">{ return }</span> // 1) Delete the trailing punctuation (1 or 2 chars) - <span class="cov7" title="8">delStart := Position{Line: lineIdx, Character: lastNonSpace + 1 - removeCount} + <span class="cov7" title="9">delStart := Position{Line: lineIdx, Character: lastNonSpace + 1 - removeCount} delEnd := Position{Line: lineIdx, Character: lastNonSpace + 1} // 2) Insert two newlines and the response at end-of-line, then one extra blank line insPos := Position{Line: lineIdx, Character: len(d.lines[lineIdx])} @@ -5838,33 +5840,33 @@ func (s *Server) buildChatHistory(uri string, lineIdx int, currentPrompt string) <span class="cov6" title="7">if !strings.HasPrefix(strings.TrimSpace(d.lines[i]), ">") </span><span class="cov5" title="5">{ break</span> } - <span class="cov3" title="2">var replyLines []string - for i >= 0 </span><span class="cov5" title="4">{ + <span class="cov2" title="2">var replyLines []string + for i >= 0 </span><span class="cov4" title="4">{ line := strings.TrimSpace(d.lines[i]) - if strings.HasPrefix(line, ">") </span><span class="cov3" title="2">{ + if strings.HasPrefix(line, ">") </span><span class="cov2" title="2">{ replyLines = append([]string{strings.TrimSpace(strings.TrimPrefix(line, ">"))}, replyLines...) i-- continue</span> } - <span class="cov3" title="2">break</span> + <span class="cov2" title="2">break</span> } - <span class="cov3" title="2">for i >= 0 && strings.TrimSpace(d.lines[i]) == "" </span><span class="cov0" title="0">{ + <span class="cov2" title="2">for i >= 0 && strings.TrimSpace(d.lines[i]) == "" </span><span class="cov0" title="0">{ i-- }</span> - <span class="cov3" title="2">if i < 0 </span><span class="cov0" title="0">{ + <span class="cov2" title="2">if i < 0 </span><span class="cov0" title="0">{ break</span> } - <span class="cov3" title="2">q := strings.TrimSpace(d.lines[i]) + <span class="cov2" title="2">q := strings.TrimSpace(d.lines[i]) q = s.stripTrailingTrigger(q) pairs = append([]pair{{q: q, a: strings.Join(replyLines, "\n")}}, pairs...) i--</span> } <span class="cov7" title="9">msgs := make([]llm.Message, 0, len(pairs)*2+1) - for _, p := range pairs </span><span class="cov3" title="2">{ - if strings.TrimSpace(p.q) != "" </span><span class="cov3" title="2">{ + for _, p := range pairs </span><span class="cov2" title="2">{ + if strings.TrimSpace(p.q) != "" </span><span class="cov2" title="2">{ msgs = append(msgs, llm.Message{Role: "user", Content: p.q}) }</span> - <span class="cov3" title="2">if strings.TrimSpace(p.a) != "" </span><span class="cov3" title="2">{ + <span class="cov2" title="2">if strings.TrimSpace(p.a) != "" </span><span class="cov2" title="2">{ msgs = append(msgs, llm.Message{Role: "assistant", Content: p.a}) }</span> } @@ -5873,12 +5875,12 @@ func (s *Server) buildChatHistory(uri string, lineIdx int, currentPrompt string) } // stripTrailingTrigger removes the trailing chat trigger punctuation from a line if present. -func (s *Server) stripTrailingTrigger(sx string) string <span class="cov9" title="16">{ +func (s *Server) stripTrailingTrigger(sx string) string <span class="cov9" title="17">{ trim := strings.TrimRight(sx, " \t") if len(trim) == 0 </span><span class="cov0" title="0">{ return sx }</span> - <span class="cov9" title="16">_, prefixes, suffixChar := s.chatConfig() + <span class="cov9" title="17">_, prefixes, suffixChar := s.chatConfig() if len(trim) >= 2 && suffixChar != 0 && trim[len(trim)-1] == suffixChar </span><span class="cov5" title="5">{ prev := string(trim[len(trim)-2]) for _, pf := range prefixes </span><span class="cov7" title="11">{ @@ -5887,11 +5889,11 @@ func (s *Server) stripTrailingTrigger(sx string) string <span class="cov9" title }</span> } } - <span class="cov7" title="11">last := trim[len(trim)-1] + <span class="cov8" title="12">last := trim[len(trim)-1] switch last </span>{ - case '?', '!', ':':<span class="cov7" title="8"> + case '?', '!', ':':<span class="cov6" title="8"> return strings.TrimRight(trim[:len(trim)-1], " \t")</span> - default:<span class="cov4" title="3"> + default:<span class="cov4" title="4"> return sx</span> } } @@ -5900,7 +5902,7 @@ func (s *Server) stripTrailingTrigger(sx string) string <span class="cov9" title // - system from prompts.chat.system // - rolling in-editor history up to current prompt // - optional extra context per general.context_mode (window/full-file/new-func) -func (s *Server) buildChatMessages(uri string, pos Position, prompt string) []llm.Message <span class="cov7" title="8">{ +func (s *Server) buildChatMessages(uri string, pos Position, prompt string) []llm.Message <span class="cov6" title="8">{ // Base system and history cfg := s.currentConfig() sys := cfg.PromptChatSystem @@ -5920,12 +5922,12 @@ func (s *Server) buildChatMessages(uri string, pos Position, prompt string) []ll <span class="cov4" title="3">msgs = append(msgs, llm.Message{Role: "user", Content: header})</span> } // Then add history (which ends with the current prompt) - <span class="cov7" title="8">msgs = append(msgs, history...) + <span class="cov6" title="8">msgs = append(msgs, history...) return msgs</span> } // clientApplyEdit sends a workspace/applyEdit request to the client. -func (s *Server) clientApplyEdit(label string, edit WorkspaceEdit) <span class="cov7" title="8">{ +func (s *Server) clientApplyEdit(label string, edit WorkspaceEdit) <span class="cov7" title="9">{ params := ApplyWorkspaceEditParams{Label: label, Edit: edit} id := s.nextReqID() req := Request{JSONRPC: "2.0", ID: id, Method: "workspace/applyEdit"} @@ -5935,7 +5937,7 @@ func (s *Server) clientApplyEdit(label string, edit WorkspaceEdit) <span class=" }</span> // nextReqID returns a unique json.RawMessage id for server-initiated requests. -func (s *Server) nextReqID() json.RawMessage <span class="cov7" title="11">{ +func (s *Server) nextReqID() json.RawMessage <span class="cov8" title="12">{ s.mu.Lock() s.nextID++ idNum := s.nextID @@ -6784,8 +6786,8 @@ func (s *Server) currentLLMClient() llm.Client <span class="cov8" title="199">{ return s.llmClient }</span> -func (s *Server) currentConfig() appconfig.App <span class="cov10" title="407">{ - if s.configStore != nil </span><span class="cov2" title="2">{ +func (s *Server) currentConfig() appconfig.App <span class="cov10" title="409">{ + if s.configStore != nil </span><span class="cov3" title="4">{ return s.configStore.Snapshot() }</span> <span class="cov9" title="405">s.mu.RLock() @@ -6879,10 +6881,10 @@ func (s *Server) inlineMarkers() (open string, close string, openChar byte, clos <span class="cov7" title="88">return open, close, openChar, closeChar</span> } -func (s *Server) chatConfig() (suffix string, prefixes []string, suffixChar byte) <span class="cov6" title="44">{ +func (s *Server) chatConfig() (suffix string, prefixes []string, suffixChar byte) <span class="cov6" title="46">{ cfg := s.currentConfig() suffix = cfg.ChatSuffix - if suffix != "" </span><span class="cov6" title="42">{ + if suffix != "" </span><span class="cov6" title="44">{ suffix = strings.TrimSpace(suffix) if suffix == "" </span><span class="cov0" title="0">{ suffix = ">" @@ -6890,16 +6892,16 @@ func (s *Server) chatConfig() (suffix string, prefixes []string, suffixChar byte } else<span class="cov2" title="2"> { suffix = "" }</span> - <span class="cov6" title="44">if len(cfg.ChatPrefixes) == 0 </span><span class="cov0" title="0">{ + <span class="cov6" title="46">if len(cfg.ChatPrefixes) == 0 </span><span class="cov0" title="0">{ prefixes = []string{"?", "!", ":", ";"} - }</span> else<span class="cov6" title="44"> { + }</span> else<span class="cov6" title="46"> { prefixes = append([]string{}, cfg.ChatPrefixes...) }</span> - <span class="cov6" title="44">suffixChar = '>' - if len(suffix) > 0 </span><span class="cov6" title="42">{ + <span class="cov6" title="46">suffixChar = '>' + if len(suffix) > 0 </span><span class="cov6" title="44">{ suffixChar = suffix[0] }</span> - <span class="cov6" title="44">return suffix, prefixes, suffixChar</span> + <span class="cov6" title="46">return suffix, prefixes, suffixChar</span> } func (s *Server) promptSet() appconfig.App <span class="cov2" title="2">{ @@ -7002,7 +7004,7 @@ func (s *Server) readMessage() ([]byte, error) <span class="cov2" title="2">{ <span class="cov1" title="1">return buf, nil</span> } -func (s *Server) writeMessage(v any) <span class="cov10" title="41">{ +func (s *Server) writeMessage(v any) <span class="cov10" title="42">{ s.outMu.Lock() defer s.outMu.Unlock() @@ -7011,12 +7013,12 @@ func (s *Server) writeMessage(v any) <span class="cov10" title="41">{ logging.Logf("lsp ", "marshal error: %v", err) return }</span> - <span class="cov10" title="41">header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(data)) + <span class="cov10" title="42">header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(data)) if _, err := io.WriteString(s.out, header); err != nil </span><span class="cov0" title="0">{ logging.Logf("lsp ", "write header error: %v", err) return }</span> - <span class="cov10" title="41">if _, err := s.out.Write(data); err != nil </span><span class="cov0" title="0">{ + <span class="cov10" title="42">if _, err := s.out.Write(data); err != nil </span><span class="cov0" title="0">{ logging.Logf("lsp ", "write body error: %v", err) return }</span> @@ -7056,12 +7058,12 @@ type Store struct { } // New creates a Store seeded with the provided configuration snapshot. -func New(cfg appconfig.App) *Store <span class="cov4" title="11">{ +func New(cfg appconfig.App) *Store <span class="cov4" title="12">{ return &Store{cfg: cfg, listeners: make(map[int]Listener)} }</span> // Snapshot returns the current configuration snapshot. Callers must treat it as read-only. -func (s *Store) Snapshot() appconfig.App <span class="cov3" title="4">{ +func (s *Store) Snapshot() appconfig.App <span class="cov3" title="6">{ s.mu.RLock() defer s.mu.RUnlock() return s.cfg @@ -7087,7 +7089,7 @@ func (s *Store) Subscribe(listener Listener) func() <span class="cov2" title="2" // Set replaces the current configuration with the provided snapshot and notifies listeners. // It returns the list of detected changes between the previous and new configuration. -func (s *Store) Set(cfg appconfig.App) []Change <span class="cov3" title="4">{ +func (s *Store) Set(cfg appconfig.App) []Change <span class="cov3" title="5">{ s.mu.Lock() old := s.cfg s.cfg = cfg @@ -7095,108 +7097,108 @@ func (s *Store) Set(cfg appconfig.App) []Change <span class="cov3" title="4">{ for _, l := range s.listeners </span><span class="cov1" title="1">{ listeners = append(listeners, l) }</span> - <span class="cov3" title="4">s.mu.Unlock() + <span class="cov3" title="5">s.mu.Unlock() changes := Diff(old, cfg) for _, l := range listeners </span><span class="cov1" title="1">{ l(old, cfg) }</span> - <span class="cov3" title="4">return changes</span> + <span class="cov3" title="5">return changes</span> } // Reload re-reads configuration using the supplied options and applies it when valid. -func (s *Store) Reload(logger *log.Logger, opts appconfig.LoadOptions) ([]Change, error) <span class="cov2" title="2">{ +func (s *Store) Reload(logger *log.Logger, opts appconfig.LoadOptions) ([]Change, error) <span class="cov2" title="3">{ cfg := appconfig.LoadWithOptions(logger, opts) if err := cfg.Validate(); err != nil </span><span class="cov0" title="0">{ return nil, err }</span> - <span class="cov2" title="2">return s.Set(cfg), nil</span> + <span class="cov2" title="3">return s.Set(cfg), nil</span> } // Diff computes a stable, sorted list of key/value changes between two configuration snapshots. -func Diff(oldCfg, newCfg appconfig.App) []Change <span class="cov3" title="4">{ +func Diff(oldCfg, newCfg appconfig.App) []Change <span class="cov3" title="5">{ before := flattenAppConfig(oldCfg) after := flattenAppConfig(newCfg) keys := make(map[string]struct{}, len(before)+len(after)) - for k := range before </span><span class="cov7" title="100">{ + for k := range before </span><span class="cov8" title="125">{ keys[k] = struct{}{} }</span> - <span class="cov3" title="4">for k := range after </span><span class="cov7" title="100">{ + <span class="cov3" title="5">for k := range after </span><span class="cov8" title="125">{ keys[k] = struct{}{} }</span> - <span class="cov3" title="4">ordered := make([]string, 0, len(keys)) - for k := range keys </span><span class="cov7" title="100">{ + <span class="cov3" title="5">ordered := make([]string, 0, len(keys)) + for k := range keys </span><span class="cov8" title="125">{ ordered = append(ordered, k) }</span> - <span class="cov3" title="4">sort.Strings(ordered) + <span class="cov3" title="5">sort.Strings(ordered) changes := make([]Change, 0, len(ordered)) - for _, k := range ordered </span><span class="cov7" title="100">{ - if before[k] == after[k] </span><span class="cov7" title="95">{ + for _, k := range ordered </span><span class="cov8" title="125">{ + if before[k] == after[k] </span><span class="cov8" title="120">{ continue</span> } <span class="cov3" title="5">changes = append(changes, Change{Key: k, Old: before[k], New: after[k]})</span> } - <span class="cov3" title="4">return changes</span> + <span class="cov3" title="5">return changes</span> } -func flattenAppConfig(cfg appconfig.App) map[string]string <span class="cov4" title="8">{ +func flattenAppConfig(cfg appconfig.App) map[string]string <span class="cov4" title="10">{ result := make(map[string]string) val := reflect.ValueOf(cfg) typ := val.Type() - for i := 0; i < typ.NumField(); i++ </span><span class="cov10" title="376">{ + for i := 0; i < typ.NumField(); i++ </span><span class="cov10" title="470">{ field := typ.Field(i) key := strings.TrimSpace(field.Tag.Get("toml")) - if key == "" || key == "-" </span><span class="cov8" title="184">{ + if key == "" || key == "-" </span><span class="cov8" title="230">{ switch field.Name </span>{ - case "StatsWindowMinutes":<span class="cov4" title="8"> + case "StatsWindowMinutes":<span class="cov4" title="10"> key = "stats_window_minutes"</span> - default:<span class="cov8" title="176"> + default:<span class="cov8" title="220"> continue</span> } } - <span class="cov9" title="200">if idx := strings.Index(key, ","); idx >= 0 </span><span class="cov0" title="0">{ + <span class="cov9" title="250">if idx := strings.Index(key, ","); idx >= 0 </span><span class="cov0" title="0">{ key = key[:idx] }</span> - <span class="cov9" title="200">if key == "" || key == "-" </span><span class="cov0" title="0">{ + <span class="cov9" title="250">if key == "" || key == "-" </span><span class="cov0" title="0">{ continue</span> } - <span class="cov9" title="200">result[key] = stringifyValue(val.Field(i))</span> + <span class="cov9" title="250">result[key] = stringifyValue(val.Field(i))</span> } - <span class="cov4" title="8">return result</span> + <span class="cov4" title="10">return result</span> } -func stringifyValue(v reflect.Value) string <span class="cov9" title="224">{ +func stringifyValue(v reflect.Value) string <span class="cov9" title="282">{ if !v.IsValid() </span><span class="cov0" title="0">{ return "" }</span> - <span class="cov9" title="224">switch v.Kind() </span>{ - case reflect.String:<span class="cov7" title="88"> + <span class="cov9" title="282">switch v.Kind() </span>{ + case reflect.String:<span class="cov7" title="110"> return v.String()</span> - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:<span class="cov7" title="64"> + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:<span class="cov7" title="80"> return strconv.FormatInt(v.Int(), 10)</span> case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:<span class="cov0" title="0"> return strconv.FormatUint(v.Uint(), 10)</span> - case reflect.Float32, reflect.Float64:<span class="cov5" title="24"> + case reflect.Float32, reflect.Float64:<span class="cov6" title="32"> return strconv.FormatFloat(v.Float(), 'f', -1, 64)</span> case reflect.Bool:<span class="cov0" title="0"> return strconv.FormatBool(v.Bool())</span> - case reflect.Slice:<span class="cov5" title="16"> - if v.IsNil() </span><span class="cov4" title="10">{ + case reflect.Slice:<span class="cov5" title="20"> + if v.IsNil() </span><span class="cov4" title="12">{ return "" }</span> - <span class="cov3" title="6">if v.Type().Elem().Kind() == reflect.String </span><span class="cov3" title="6">{ + <span class="cov4" title="8">if v.Type().Elem().Kind() == reflect.String </span><span class="cov4" title="8">{ parts := make([]string, v.Len()) - for i := range parts </span><span class="cov5" title="24">{ + for i := range parts </span><span class="cov6" title="32">{ parts[i] = v.Index(i).String() }</span> - <span class="cov3" title="6">return strings.Join(parts, ",")</span> + <span class="cov4" title="8">return strings.Join(parts, ",")</span> } <span class="cov0" title="0">return fmt.Sprint(v.Interface())</span> - case reflect.Ptr:<span class="cov6" title="32"> + case reflect.Ptr:<span class="cov6" title="40"> if v.IsNil() </span><span class="cov4" title="8">{ return "(unset)" }</span> - <span class="cov5" title="24">return stringifyValue(v.Elem())</span> + <span class="cov6" title="32">return stringifyValue(v.Elem())</span> default:<span class="cov0" title="0"> return fmt.Sprint(v.Interface())</span> } @@ -7213,9 +7215,9 @@ import ( "golang.org/x/sys/unix" ) -func tryLockFile(fd uintptr) error <span class="cov10" title="198">{ - if err := unix.Flock(int(fd), unix.LOCK_EX|unix.LOCK_NB); err != nil </span><span class="cov9" title="121">{ - if errors.Is(err, unix.EWOULDBLOCK) </span><span class="cov9" title="121">{ +func tryLockFile(fd uintptr) error <span class="cov10" title="213">{ + if err := unix.Flock(int(fd), unix.LOCK_EX|unix.LOCK_NB); err != nil </span><span class="cov9" title="136">{ + if errors.Is(err, unix.EWOULDBLOCK) </span><span class="cov9" title="136">{ return errLockWouldBlock }</span> <span class="cov0" title="0">return err</span> @@ -7383,18 +7385,18 @@ func Update(ctx context.Context, provider, model string, sentBytes, recvBytes in func acquireFileLock(ctx context.Context, f *os.File) (func() error, error) <span class="cov5" title="77">{ fd := f.Fd() - for </span><span class="cov6" title="198">{ + for </span><span class="cov6" title="213">{ err := tryLockFile(fd) if err == nil </span><span class="cov5" title="77">{ return func() error </span><span class="cov5" title="77">{ return unlockFile(fd) }</span>, nil } - <span class="cov5" title="121">if errors.Is(err, errLockWouldBlock) </span><span class="cov5" title="121">{ + <span class="cov5" title="136">if errors.Is(err, errLockWouldBlock) </span><span class="cov5" title="136">{ select </span>{ case <-ctx.Done():<span class="cov0" title="0"> return nil, ctx.Err()</span> - case <-time.After(5 * time.Millisecond):<span class="cov5" title="121"></span> + case <-time.After(5 * time.Millisecond):<span class="cov5" title="136"></span> } - <span class="cov5" title="121">continue</span> + <span class="cov5" title="136">continue</span> } <span class="cov0" title="0">return nil, err</span> } @@ -7426,18 +7428,18 @@ func TakeSnapshot() (Snapshot, error) <span class="cov5" title="69">{ }</span> <span class="cov5" title="69">cutoff := time.Now().Add(-win) snap := Snapshot{Providers: make(map[string]ProviderEntry), Window: win} - for _, ev := range sf.Events </span><span class="cov10" title="5778">{ + for _, ev := range sf.Events </span><span class="cov10" title="7296">{ if ev.TS.Before(cutoff) </span><span class="cov0" title="0">{ continue</span> } - <span class="cov10" title="5778">snap.Global.Reqs++ + <span class="cov10" title="7296">snap.Global.Reqs++ snap.Global.Sent += ev.Sent snap.Global.Recv += ev.Recv pe := snap.Providers[ev.Provider] if pe.Models == nil </span><span class="cov7" title="465">{ pe.Models = make(map[string]Counters) }</span> - <span class="cov10" title="5778">pe.Totals.Reqs++ + <span class="cov10" title="7296">pe.Totals.Reqs++ pe.Totals.Sent += ev.Sent pe.Totals.Recv += ev.Recv mc := pe.Models[ev.Model] @@ -7524,23 +7526,23 @@ import "fmt" // HumanBytes renders n in a short human-friendly form using base-1000 units. // Examples: 999 -> 999B, 1200 -> 1.2k, 1540000 -> 1.5M func HumanBytes(n int64) string <span class="cov10" title="138">{ - if n < 1000 </span><span class="cov2" title="2">{ + if n < 1000 </span><span class="cov7" title="35">{ return fmt.Sprintf("%dB", n) }</span> - <span class="cov9" title="136">const unit = 1000.0 + <span class="cov9" title="103">const unit = 1000.0 v := float64(n) suffix := []string{"k", "M", "G", "T"} i := 0 - for v >= unit && i < len(suffix)-1 </span><span class="cov9" title="136">{ + for v >= unit && i < len(suffix)-1 </span><span class="cov9" title="103">{ v /= unit i++ }</span> - <span class="cov9" title="136">s := fmt.Sprintf("%.1f%s", v, suffix[i]) + <span class="cov9" title="103">s := fmt.Sprintf("%.1f%s", v, suffix[i]) // Strip trailing ".0" if len(s) >= 3 && s[len(s)-2:] == ".0" </span><span class="cov0" title="0">{ s = fmt.Sprintf("%d%s", int(v), suffix[i]) }</span> - <span class="cov9" title="136">return s</span> + <span class="cov9" title="103">return s</span> } </pre> |
