From d0330d02ff040326216ab940a767490cb2de09ce Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 26 Sep 2025 07:46:14 +0300 Subject: Allow slash commands without prefix --- docs/coverage.html | 566 +++++++++++++++++++++++++++-------------------------- 1 file changed, 284 insertions(+), 282 deletions(-) (limited to 'docs/coverage.html') 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 @@ - + @@ -111,7 +111,7 @@ - + @@ -353,7 +353,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 @@ -409,7 +409,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 { @@ -418,31 +418,31 @@ 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 { return cfg // Return defaults if no logger is provided (e.g. in tests) } - configPath, err := getConfigPath() + configPath, err := getConfigPath() if err != nil { logger.Printf("%v", err) // Even if config path cannot be resolved, keep defaults and optionally apply env overrides below. - } else { - if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil { + } else { + if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil { cfg.mergeWith(fileCfg) } // When the config file is missing or invalid, we keep defaults and still // apply any environment overrides below (unless disabled). } - if !opts.IgnoreEnv { + if !opts.IgnoreEnv { // Environment overrides (take precedence over file) if envCfg := loadFromEnv(logger); envCfg != nil { cfg.mergeWith(envCfg) } } - return cfg + return cfg } // Private helpers @@ -511,16 +511,16 @@ 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 != "" { @@ -609,11 +609,11 @@ 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, @@ -625,13 +625,13 @@ func (fc *fileConfig) toApp() App { } // 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 != sectionCompletion{}) { tmp := App{ CompletionDebounceMs: fc.Completion.CompletionDebounceMs, CompletionThrottleMs: fc.Completion.CompletionThrottleMs, @@ -641,31 +641,31 @@ func (fc *fileConfig) toApp() App { } // 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(), @@ -675,7 +675,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, @@ -685,7 +685,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, @@ -696,7 +696,7 @@ func (fc *fileConfig) toApp() App { // prompts // completion - if (fc.Prompts.Completion != sectionPromptsCompletion{}) { + if (fc.Prompts.Completion != sectionPromptsCompletion{}) { if strings.TrimSpace(fc.Prompts.Completion.SystemGeneral) != "" { out.PromptCompletionSystemGeneral = fc.Prompts.Completion.SystemGeneral } @@ -717,11 +717,11 @@ func (fc *fileConfig) toApp() App { } } // 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) != "" || @@ -778,7 +778,7 @@ func (fc *fileConfig) toApp() App { } } // cli - if (fc.Prompts.CLI != sectionPromptsCLI{}) { + if (fc.Prompts.CLI != sectionPromptsCLI{}) { if strings.TrimSpace(fc.Prompts.CLI.DefaultSystem) != "" { out.PromptCLIDefaultSystem = fc.Prompts.CLI.DefaultSystem } @@ -787,24 +787,24 @@ func (fc *fileConfig) toApp() App { } } // 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 !os.IsNotExist(err) && logger != nil { @@ -813,7 +813,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) 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 @@ -826,7 +826,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) 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": {}, @@ -835,8 +835,8 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) { - if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable { + for k := range raw { + if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable { continue } if _, isLegacy := legacy[k]; isLegacy { @@ -844,13 +844,13 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) 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 { @@ -864,7 +864,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) if t, ok := raw["logging"].(map[string]any); ok { + if t, ok := raw["logging"].(map[string]any); ok { if v, present := t["log_preview_limit"]; present { switch vv := v.(type) { case int64: @@ -876,142 +876,142 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) return &tab, nil + return &tab, nil } -func (a *App) mergeWith(other *App) { +func (a *App) mergeWith(other *App) { a.mergeBasics(other) a.mergeProviderFields(other) a.mergePrompts(other) } // 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.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 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.ChatPrefixes) > 0 { a.ChatPrefixes = slices.Clone(other.ChatPrefixes) } - if s := strings.TrimSpace(other.Provider); s != "" { + if s := strings.TrimSpace(other.Provider); s != "" { a.Provider = s } } // mergePrompts copies non-empty prompt templates from other. -func (a *App) mergePrompts(other *App) { +func (a *App) mergePrompts(other *App) { // Completion if strings.TrimSpace(other.PromptCompletionSystemGeneral) != "" { a.PromptCompletionSystemGeneral = other.PromptCompletionSystemGeneral } - if strings.TrimSpace(other.PromptCompletionSystemParams) != "" { + if strings.TrimSpace(other.PromptCompletionSystemParams) != "" { a.PromptCompletionSystemParams = other.PromptCompletionSystemParams } - if strings.TrimSpace(other.PromptCompletionSystemInline) != "" { + if strings.TrimSpace(other.PromptCompletionSystemInline) != "" { a.PromptCompletionSystemInline = other.PromptCompletionSystemInline } - if strings.TrimSpace(other.PromptCompletionUserGeneral) != "" { + if strings.TrimSpace(other.PromptCompletionUserGeneral) != "" { a.PromptCompletionUserGeneral = other.PromptCompletionUserGeneral } - if strings.TrimSpace(other.PromptCompletionUserParams) != "" { + if strings.TrimSpace(other.PromptCompletionUserParams) != "" { a.PromptCompletionUserParams = other.PromptCompletionUserParams } - if strings.TrimSpace(other.PromptCompletionExtraHeader) != "" { + if strings.TrimSpace(other.PromptCompletionExtraHeader) != "" { a.PromptCompletionExtraHeader = other.PromptCompletionExtraHeader } // Provider-native - if strings.TrimSpace(other.PromptNativeCompletion) != "" { + if strings.TrimSpace(other.PromptNativeCompletion) != "" { a.PromptNativeCompletion = other.PromptNativeCompletion } // Chat - if strings.TrimSpace(other.PromptChatSystem) != "" { + if strings.TrimSpace(other.PromptChatSystem) != "" { a.PromptChatSystem = other.PromptChatSystem } // Code actions - if strings.TrimSpace(other.PromptCodeActionRewriteSystem) != "" { + if strings.TrimSpace(other.PromptCodeActionRewriteSystem) != "" { a.PromptCodeActionRewriteSystem = other.PromptCodeActionRewriteSystem } - if strings.TrimSpace(other.PromptCodeActionDiagnosticsSystem) != "" { + if strings.TrimSpace(other.PromptCodeActionDiagnosticsSystem) != "" { a.PromptCodeActionDiagnosticsSystem = other.PromptCodeActionDiagnosticsSystem } - if strings.TrimSpace(other.PromptCodeActionDocumentSystem) != "" { + if strings.TrimSpace(other.PromptCodeActionDocumentSystem) != "" { a.PromptCodeActionDocumentSystem = other.PromptCodeActionDocumentSystem } - if strings.TrimSpace(other.PromptCodeActionRewriteUser) != "" { + if strings.TrimSpace(other.PromptCodeActionRewriteUser) != "" { a.PromptCodeActionRewriteUser = other.PromptCodeActionRewriteUser } - if strings.TrimSpace(other.PromptCodeActionDiagnosticsUser) != "" { + if strings.TrimSpace(other.PromptCodeActionDiagnosticsUser) != "" { a.PromptCodeActionDiagnosticsUser = other.PromptCodeActionDiagnosticsUser } - if strings.TrimSpace(other.PromptCodeActionDocumentUser) != "" { + if strings.TrimSpace(other.PromptCodeActionDocumentUser) != "" { a.PromptCodeActionDocumentUser = other.PromptCodeActionDocumentUser } - if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" { + if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" { a.PromptCodeActionGoTestSystem = other.PromptCodeActionGoTestSystem } - if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" { + if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" { a.PromptCodeActionGoTestUser = other.PromptCodeActionGoTestUser } - if strings.TrimSpace(other.PromptCodeActionSimplifySystem) != "" { + if strings.TrimSpace(other.PromptCodeActionSimplifySystem) != "" { a.PromptCodeActionSimplifySystem = other.PromptCodeActionSimplifySystem } - if strings.TrimSpace(other.PromptCodeActionSimplifyUser) != "" { + if strings.TrimSpace(other.PromptCodeActionSimplifyUser) != "" { a.PromptCodeActionSimplifyUser = other.PromptCodeActionSimplifyUser } // CLI - if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" { + if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" { a.PromptCLIDefaultSystem = other.PromptCLIDefaultSystem } - if strings.TrimSpace(other.PromptCLIExplainSystem) != "" { + if strings.TrimSpace(other.PromptCLIExplainSystem) != "" { a.PromptCLIExplainSystem = other.PromptCLIExplainSystem } // Custom actions - if len(other.CustomActions) > 0 { + if len(other.CustomActions) > 0 { a.CustomActions = append([]CustomAction{}, other.CustomActions...) } - if strings.TrimSpace(other.TmuxCustomMenuHotkey) != "" { + if strings.TrimSpace(other.TmuxCustomMenuHotkey) != "" { a.TmuxCustomMenuHotkey = other.TmuxCustomMenuHotkey } } // Validate checks custom actions and tmux settings for duplicates and consistency. -func (a App) Validate() error { +func (a App) Validate() error { // 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 { } } // Tmux custom menu hotkey validation - if hk := strings.TrimSpace(a.TmuxCustomMenuHotkey); hk != "" { + if hk := strings.TrimSpace(a.TmuxCustomMenuHotkey); hk != "" { if len([]rune(hk)) != 1 { return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s", hk) } @@ -1064,43 +1064,43 @@ func (a App) Validate() error { return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s (clashes with built-in)", hk) } } - return nil + return nil } // mergeProviderFields merges per-provider configuration. -func (a *App) mergeProviderFields(other *App) { - if s := strings.TrimSpace(other.OpenAIBaseURL); s != "" { +func (a *App) mergeProviderFields(other *App) { + if s := strings.TrimSpace(other.OpenAIBaseURL); s != "" { a.OpenAIBaseURL = s } - if s := strings.TrimSpace(other.OpenAIModel); s != "" { + if s := strings.TrimSpace(other.OpenAIModel); s != "" { a.OpenAIModel = s } - if other.OpenAITemperature != nil { // allow explicit 0.0 + if other.OpenAITemperature != nil { // allow explicit 0.0 a.OpenAITemperature = other.OpenAITemperature } - if s := strings.TrimSpace(other.OllamaBaseURL); s != "" { + if s := strings.TrimSpace(other.OllamaBaseURL); s != "" { a.OllamaBaseURL = s } - if s := strings.TrimSpace(other.OllamaModel); s != "" { + if s := strings.TrimSpace(other.OllamaModel); s != "" { a.OllamaModel = s } - if other.OllamaTemperature != nil { // allow explicit 0.0 + if other.OllamaTemperature != nil { // allow explicit 0.0 a.OllamaTemperature = other.OllamaTemperature } - if s := strings.TrimSpace(other.CopilotBaseURL); s != "" { + if s := strings.TrimSpace(other.CopilotBaseURL); s != "" { a.CopilotBaseURL = s } - if s := strings.TrimSpace(other.CopilotModel); s != "" { + if s := strings.TrimSpace(other.CopilotModel); s != "" { a.CopilotModel = s } - if other.CopilotTemperature != nil { // allow explicit 0.0 + if other.CopilotTemperature != nil { // allow explicit 0.0 a.CopilotTemperature = other.CopilotTemperature } } -func getConfigPath() (string, error) { +func getConfigPath() (string, error) { var configPath string - if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { configPath = filepath.Join(xdgConfigHome, "hexai", "config.toml") } else { home, err := os.UserHomeDir() @@ -1109,22 +1109,22 @@ func getConfigPath() (string, error) { } configPath = filepath.Join(home, ".config", "hexai", "config.toml") } - return configPath, nil + return configPath, nil } // --- 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 { +func loadFromEnv(logger *log.Logger) *App { var out App var any bool // helpers - getenv := func(k string) string { return strings.TrimSpace(os.Getenv(k)) } - parseInt := func(k string) (int, bool) { + getenv := func(k string) string { return strings.TrimSpace(os.Getenv(k)) } + parseInt := func(k string) (int, bool) { v := getenv(k) - if v == "" { + if v == "" { return 0, false } n, err := strconv.Atoi(v) @@ -1136,9 +1136,9 @@ func loadFromEnv(logger *log.Logger) *App { } return n, true } - parseFloatPtr := func(k string) (*float64, bool) { + parseFloatPtr := func(k string) (*float64, bool) { v := getenv(k) - if v == "" { + if v == "" { return nil, false } f, err := strconv.ParseFloat(v, 64) @@ -1151,43 +1151,43 @@ func loadFromEnv(logger *log.Logger) *App { return &f, true } - if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok { + if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok { out.MaxTokens = n any = true } - if s := getenv("HEXAI_CONTEXT_MODE"); s != "" { + if s := getenv("HEXAI_CONTEXT_MODE"); s != "" { out.ContextMode = s any = true } - if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok { + if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok { out.ContextWindowLines = n any = true } - if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok { + if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok { out.MaxContextTokens = n any = true } - if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok { + if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok { out.LogPreviewLimit = n any = true } - if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok { + if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok { out.ManualInvokeMinPrefix = n any = true } - if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok { + if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok { out.CompletionDebounceMs = n any = true } - if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok { + if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok { out.CompletionThrottleMs = n any = true } - if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok { + if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok { out.CodingTemperature = f any = true } - if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" { + if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" { parts := strings.Split(s, ",") out.TriggerCharacters = nil for _, p := range parts { @@ -1197,19 +1197,19 @@ func loadFromEnv(logger *log.Logger) *App { } any = true } - if s := getenv("HEXAI_INLINE_OPEN"); s != "" { + if s := getenv("HEXAI_INLINE_OPEN"); s != "" { out.InlineOpen = s any = true } - if s := getenv("HEXAI_INLINE_CLOSE"); s != "" { + if s := getenv("HEXAI_INLINE_CLOSE"); s != "" { out.InlineClose = s any = true } - if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" { + if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" { out.ChatSuffix = s any = true } - if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" { + if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" { parts := strings.Split(s, ",") out.ChatPrefixes = nil for _, p := range parts { @@ -1219,17 +1219,17 @@ func loadFromEnv(logger *log.Logger) *App { } any = true } - if s := getenv("HEXAI_PROVIDER"); s != "" { + if s := getenv("HEXAI_PROVIDER"); s != "" { out.Provider = s any = true } - modelForce := strings.TrimSpace(getenv("HEXAI_MODEL_FORCE")) + 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) { + pickModel := func(providerName, specific string) (string, bool) { specific = strings.TrimSpace(specific) nameLower := strings.ToLower(strings.TrimSpace(providerName)) if modelForce != "" { @@ -1242,10 +1242,10 @@ func loadFromEnv(logger *log.Logger) *App { return modelForce, true } } - if specific != "" { + if specific != "" { return specific, true } - if modelGeneric != "" { + if modelGeneric != "" { if providerLower == nameLower { return modelGeneric, true } @@ -1254,50 +1254,50 @@ func loadFromEnv(logger *log.Logger) *App { return modelGeneric, true } } - return "", false + return "", false } // Provider-specific - if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" { + if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" { out.OpenAIBaseURL = s any = true } - if model, ok := pickModel("openai", getenv("HEXAI_OPENAI_MODEL")); ok { + if model, ok := pickModel("openai", getenv("HEXAI_OPENAI_MODEL")); ok { out.OpenAIModel = model any = true } - if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok { + if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok { out.OpenAITemperature = f any = true } - if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" { + if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" { out.OllamaBaseURL = s any = true } - if model, ok := pickModel("ollama", getenv("HEXAI_OLLAMA_MODEL")); ok { + if model, ok := pickModel("ollama", getenv("HEXAI_OLLAMA_MODEL")); ok { out.OllamaModel = model any = true } - if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok { + if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok { out.OllamaTemperature = f any = true } - if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" { + if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" { out.CopilotBaseURL = s any = true } - if model, ok := pickModel("copilot", getenv("HEXAI_COPILOT_MODEL")); ok { + if model, ok := pickModel("copilot", getenv("HEXAI_COPILOT_MODEL")); ok { out.CopilotModel = model any = true } - if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok { + if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok { out.CopilotTemperature = f any = true } - if !any { + if !any { return nil } return &out @@ -3814,14 +3814,14 @@ type chatCommandResult struct { message string } -func (s *Server) chatCommandResponse(uri string, lineIdx int, prompt string) (chatCommandResult, bool) { +func (s *Server) chatCommandResponse(uri string, lineIdx int, prompt string) (chatCommandResult, bool) { trimmed := strings.TrimSpace(s.stripTrailingTrigger(prompt)) - if trimmed == "" || !strings.HasPrefix(trimmed, "/") { + if trimmed == "" || !strings.HasPrefix(trimmed, "/") { return chatCommandResult{}, false } - switch { - case strings.HasPrefix(trimmed, "/reload"): + switch { + case strings.HasPrefix(trimmed, "/reload"): return s.handleReloadCommand(), true case strings.HasPrefix(trimmed, "/help"): return s.handleHelpCommand(), true @@ -3838,30 +3838,30 @@ func (s *Server) handleHelpCommand() chatCommandResult -func (s *Server) handleReloadCommand() chatCommandResult { +func (s *Server) handleReloadCommand() chatCommandResult { if s.configStore == nil { return chatCommandResult{message: "Reload unavailable: no config store"} } - changes, err := s.configStore.Reload(s.logger, appconfig.LoadOptions{IgnoreEnv: true}) + changes, err := s.configStore.Reload(s.logger, appconfig.LoadOptions{IgnoreEnv: true}) if err != nil { s.logger.Printf("config reload failed: %v", err) return chatCommandResult{message: fmt.Sprintf("Reload failed: %v", err)} } - summary := formatReloadSummary(changes) + summary := formatReloadSummary(changes) s.logger.Print(summary) return chatCommandResult{message: summary} } -func formatReloadSummary(changes []runtimeconfig.Change) string { - if len(changes) == 0 { +func formatReloadSummary(changes []runtimeconfig.Change) string { + if len(changes) == 0 { return "Reloaded config (no changes detected)." } - lines := make([]string, 0, len(changes)+1) + lines := make([]string, 0, len(changes)+1) lines = append(lines, fmt.Sprintf("Reloaded config (%d changes):", len(changes))) for _, ch := range changes { lines = append(lines, fmt.Sprintf("- %s: %s → %s", ch.Key, ch.Old, ch.New)) } - return strings.Join(lines, "\n") + return strings.Join(lines, "\n") } @@ -3965,7 +3965,7 @@ type document struct { lines []string } -func (s *Server) setDocument(uri, text string) { +func (s *Server) setDocument(uri, text string) { 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() { s.mu.Unlock() } -func (s *Server) getDocument(uri string) *document { +func (s *Server) getDocument(uri string) *document { s.mu.RLock() defer s.mu.RUnlock() return s.docs[uri] } // splitLines splits the input string into lines, normalizing line endings to '\n'. -func splitLines(sx string) []string { +func splitLines(sx string) []string { sx = strings.ReplaceAll(sx, "\r\n", "\n") return strings.Split(sx, "\n") } @@ -5656,42 +5656,42 @@ func (s *Server) handleDidClose(req Request) { // 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) { +func (s *Server) docBeforeAfter(uri string, pos Position) (string, string) { d := s.getDocument(uri) - if d == nil { + if d == nil { return "", "" } // Clamp indices - line := pos.Line + line := pos.Line if line < 0 { line = 0 } - if line >= len(d.lines) { + if line >= len(d.lines) { line = len(d.lines) - 1 } - col := pos.Character + col := pos.Character if col < 0 { col = 0 } - if col > len(d.lines[line]) { + if col > len(d.lines[line]) { col = len(d.lines[line]) } // Build before - var b strings.Builder + var b strings.Builder for i := 0; i < line; i++ { b.WriteString(d.lines[i]) b.WriteByte('\n') } - b.WriteString(d.lines[line][:col]) + 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++ { + for i := line + 1; i < len(d.lines); i++ { a.WriteByte('\n') a.WriteString(d.lines[i]) } - return before, a.String() + return before, a.String() } // --- in-editor chat (";C ...") --- @@ -5699,76 +5699,78 @@ func (s *Server) docBeforeAfter(uri string, pos Position) (string, string) { +func (s *Server) detectAndHandleChat(uri string) { d := s.getDocument(uri) if d == nil || len(d.lines) == 0 { return } - suffix, prefixes, _ := s.chatConfig() - for i, raw := range d.lines { + suffix, prefixes, _ := s.chatConfig() + for i, raw := range d.lines { // Find last non-space character index j := len(raw) - 1 - for j >= 0 { + for j >= 0 { if raw[j] == ' ' || raw[j] == '\t' { j-- continue } - break + break } - if j < 0 { + if j < 0 { continue } - // Check suffix/prefix according to configuration - if suffix == "" { + // Check suffix and derive the prompt text before validating prefixes + if suffix == "" { continue } - // Last non-space must equal suffix - if string(raw[j]) != suffix { + if string(raw[j]) != suffix { continue } - // Require at least one char before suffix and that char must be in chatPrefixes - if j < 1 { + removeCount := len(suffix) + base := raw[:j+1-removeCount] + prompt := strings.TrimSpace(base) + if prompt == "" { continue } - prev := string(raw[j-1]) - isTrigger := false - for _, pfx := range prefixes { - if prev == pfx { - isTrigger = true - break + // Slash commands (`/foo>`) do not require a prefix trigger. + isCommand := strings.HasPrefix(prompt, "/") + if !isCommand { + // Require at least one char before suffix and that char must be in chatPrefixes + if j < 1 { + continue + } + prev := string(raw[j-1]) + match := false + for _, pfx := range prefixes { + if prev == pfx { + match = true + break + } + } + if !match { + continue } - } - if !isTrigger { - continue } // Avoid double-answering: if the next non-empty line starts with '>' we skip. - k := i + 1 - for k < len(d.lines) && strings.TrimSpace(d.lines[k]) == "" { + k := i + 1 + for k < len(d.lines) && strings.TrimSpace(d.lines[k]) == "" { k++ } - if k < len(d.lines) && strings.HasPrefix(strings.TrimSpace(d.lines[k]), ">") { - continue - } - // Derive prompt by removing only the trailing '>' - removeCount := len(suffix) - base := raw[:j+1-removeCount] - prompt := strings.TrimSpace(base) - if prompt == "" { + if k < len(d.lines) && strings.HasPrefix(strings.TrimSpace(d.lines[k]), ">") { continue } - lineIdx := i + lineIdx := i lastIdx := j - if resp, ok := s.chatCommandResponse(uri, lineIdx, prompt); ok { + if resp, ok := s.chatCommandResponse(uri, lineIdx, prompt); ok { msg := strings.TrimSpace(resp.message) - if msg != "" { + if msg != "" { s.applyChatEdits(uri, lineIdx, lastIdx, removeCount, "> "+msg) } - return + return } - if s.currentLLMClient() == nil { + if s.currentLLMClient() == nil { continue } - go func(prompt string, remove int) { + go func(prompt string, remove int) { 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) { if client == nil { return } - logging.Logf("lsp ", "chat llm=requesting model=%s", client.DefaultModel()) + logging.Logf("lsp ", "chat llm=requesting model=%s", client.DefaultModel()) text, err := s.chatWithStats(ctx, msgs, opts...) if err != nil { logging.Logf("lsp ", "chat llm error: %v", err) return } - out := strings.TrimSpace(stripCodeFences(text)) + out := strings.TrimSpace(stripCodeFences(text)) if out == "" { return } - s.applyChatEdits(uri, lineIdx, lastIdx, remove, "> "+out) + s.applyChatEdits(uri, lineIdx, lastIdx, remove, "> "+out) }(prompt, removeCount) // Only handle one per change tick to avoid flooding - break + break } } // 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) { +func (s *Server) applyChatEdits(uri string, lineIdx int, lastNonSpace int, removeCount int, response string) { d := s.getDocument(uri) if d == nil { return } // 1) Delete the trailing punctuation (1 or 2 chars) - delStart := Position{Line: lineIdx, Character: lastNonSpace + 1 - removeCount} + 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) if !strings.HasPrefix(strings.TrimSpace(d.lines[i]), ">") { break } - var replyLines []string - for i >= 0 { + var replyLines []string + for i >= 0 { line := strings.TrimSpace(d.lines[i]) - if strings.HasPrefix(line, ">") { + if strings.HasPrefix(line, ">") { replyLines = append([]string{strings.TrimSpace(strings.TrimPrefix(line, ">"))}, replyLines...) i-- continue } - break + break } - for i >= 0 && strings.TrimSpace(d.lines[i]) == "" { + for i >= 0 && strings.TrimSpace(d.lines[i]) == "" { i-- } - if i < 0 { + if i < 0 { break } - q := strings.TrimSpace(d.lines[i]) + q := strings.TrimSpace(d.lines[i]) q = s.stripTrailingTrigger(q) pairs = append([]pair{{q: q, a: strings.Join(replyLines, "\n")}}, pairs...) i-- } msgs := make([]llm.Message, 0, len(pairs)*2+1) - for _, p := range pairs { - if strings.TrimSpace(p.q) != "" { + for _, p := range pairs { + if strings.TrimSpace(p.q) != "" { msgs = append(msgs, llm.Message{Role: "user", Content: p.q}) } - if strings.TrimSpace(p.a) != "" { + if strings.TrimSpace(p.a) != "" { msgs = append(msgs, llm.Message{Role: "assistant", Content: p.a}) } } @@ -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 { +func (s *Server) stripTrailingTrigger(sx string) string { trim := strings.TrimRight(sx, " \t") if len(trim) == 0 { return sx } - _, prefixes, suffixChar := s.chatConfig() + _, prefixes, suffixChar := s.chatConfig() if len(trim) >= 2 && suffixChar != 0 && trim[len(trim)-1] == suffixChar { prev := string(trim[len(trim)-2]) for _, pf := range prefixes { @@ -5887,11 +5889,11 @@ func (s *Server) stripTrailingTrigger(sx string) string } } - last := trim[len(trim)-1] + last := trim[len(trim)-1] switch last { - case '?', '!', ':': + case '?', '!', ':': return strings.TrimRight(trim[:len(trim)-1], " \t") - default: + default: return sx } } @@ -5900,7 +5902,7 @@ func (s *Server) stripTrailingTrigger(sx string) string { +func (s *Server) buildChatMessages(uri string, pos Position, prompt string) []llm.Message { // 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 msgs = append(msgs, llm.Message{Role: "user", Content: header}) } // Then add history (which ends with the current prompt) - msgs = append(msgs, history...) + msgs = append(msgs, history...) return msgs } // clientApplyEdit sends a workspace/applyEdit request to the client. -func (s *Server) clientApplyEdit(label string, edit WorkspaceEdit) { +func (s *Server) clientApplyEdit(label string, edit WorkspaceEdit) { 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) { +func (s *Server) nextReqID() json.RawMessage { s.mu.Lock() s.nextID++ idNum := s.nextID @@ -6784,8 +6786,8 @@ func (s *Server) currentLLMClient() llm.Client { return s.llmClient } -func (s *Server) currentConfig() appconfig.App { - if s.configStore != nil { +func (s *Server) currentConfig() appconfig.App { + if s.configStore != nil { return s.configStore.Snapshot() } s.mu.RLock() @@ -6879,10 +6881,10 @@ func (s *Server) inlineMarkers() (open string, close string, openChar byte, clos return open, close, openChar, closeChar } -func (s *Server) chatConfig() (suffix string, prefixes []string, suffixChar byte) { +func (s *Server) chatConfig() (suffix string, prefixes []string, suffixChar byte) { cfg := s.currentConfig() suffix = cfg.ChatSuffix - if suffix != "" { + if suffix != "" { suffix = strings.TrimSpace(suffix) if suffix == "" { suffix = ">" @@ -6890,16 +6892,16 @@ func (s *Server) chatConfig() (suffix string, prefixes []string, suffixChar byte } else { suffix = "" } - if len(cfg.ChatPrefixes) == 0 { + if len(cfg.ChatPrefixes) == 0 { prefixes = []string{"?", "!", ":", ";"} - } else { + } else { prefixes = append([]string{}, cfg.ChatPrefixes...) } - suffixChar = '>' - if len(suffix) > 0 { + suffixChar = '>' + if len(suffix) > 0 { suffixChar = suffix[0] } - return suffix, prefixes, suffixChar + return suffix, prefixes, suffixChar } func (s *Server) promptSet() appconfig.App { @@ -7002,7 +7004,7 @@ func (s *Server) readMessage() ([]byte, error) { return buf, nil } -func (s *Server) writeMessage(v any) { +func (s *Server) writeMessage(v any) { s.outMu.Lock() defer s.outMu.Unlock() @@ -7011,12 +7013,12 @@ func (s *Server) writeMessage(v any) { logging.Logf("lsp ", "marshal error: %v", err) return } - header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(data)) + header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(data)) if _, err := io.WriteString(s.out, header); err != nil { logging.Logf("lsp ", "write header error: %v", err) return } - if _, err := s.out.Write(data); err != nil { + if _, err := s.out.Write(data); err != nil { logging.Logf("lsp ", "write body error: %v", err) return } @@ -7056,12 +7058,12 @@ type Store struct { } // New creates a Store seeded with the provided configuration snapshot. -func New(cfg appconfig.App) *Store { +func New(cfg appconfig.App) *Store { return &Store{cfg: cfg, listeners: make(map[int]Listener)} } // Snapshot returns the current configuration snapshot. Callers must treat it as read-only. -func (s *Store) Snapshot() appconfig.App { +func (s *Store) Snapshot() appconfig.App { s.mu.RLock() defer s.mu.RUnlock() return s.cfg @@ -7087,7 +7089,7 @@ func (s *Store) Subscribe(listener Listener) func() { +func (s *Store) Set(cfg appconfig.App) []Change { s.mu.Lock() old := s.cfg s.cfg = cfg @@ -7095,108 +7097,108 @@ func (s *Store) Set(cfg appconfig.App) []Change { for _, l := range s.listeners { listeners = append(listeners, l) } - s.mu.Unlock() + s.mu.Unlock() changes := Diff(old, cfg) for _, l := range listeners { l(old, cfg) } - return changes + return changes } // 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) { +func (s *Store) Reload(logger *log.Logger, opts appconfig.LoadOptions) ([]Change, error) { cfg := appconfig.LoadWithOptions(logger, opts) if err := cfg.Validate(); err != nil { return nil, err } - return s.Set(cfg), nil + return s.Set(cfg), nil } // Diff computes a stable, sorted list of key/value changes between two configuration snapshots. -func Diff(oldCfg, newCfg appconfig.App) []Change { +func Diff(oldCfg, newCfg appconfig.App) []Change { before := flattenAppConfig(oldCfg) after := flattenAppConfig(newCfg) keys := make(map[string]struct{}, len(before)+len(after)) - for k := range before { + for k := range before { keys[k] = struct{}{} } - for k := range after { + for k := range after { keys[k] = struct{}{} } - ordered := make([]string, 0, len(keys)) - for k := range keys { + ordered := make([]string, 0, len(keys)) + for k := range keys { ordered = append(ordered, k) } - sort.Strings(ordered) + sort.Strings(ordered) changes := make([]Change, 0, len(ordered)) - for _, k := range ordered { - if before[k] == after[k] { + for _, k := range ordered { + if before[k] == after[k] { continue } changes = append(changes, Change{Key: k, Old: before[k], New: after[k]}) } - return changes + return changes } -func flattenAppConfig(cfg appconfig.App) map[string]string { +func flattenAppConfig(cfg appconfig.App) map[string]string { result := make(map[string]string) val := reflect.ValueOf(cfg) typ := val.Type() - for i := 0; i < typ.NumField(); i++ { + for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) key := strings.TrimSpace(field.Tag.Get("toml")) - if key == "" || key == "-" { + if key == "" || key == "-" { switch field.Name { - case "StatsWindowMinutes": + case "StatsWindowMinutes": key = "stats_window_minutes" - default: + default: continue } } - if idx := strings.Index(key, ","); idx >= 0 { + if idx := strings.Index(key, ","); idx >= 0 { key = key[:idx] } - if key == "" || key == "-" { + if key == "" || key == "-" { continue } - result[key] = stringifyValue(val.Field(i)) + result[key] = stringifyValue(val.Field(i)) } - return result + return result } -func stringifyValue(v reflect.Value) string { +func stringifyValue(v reflect.Value) string { if !v.IsValid() { return "" } - switch v.Kind() { - case reflect.String: + switch v.Kind() { + case reflect.String: return v.String() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(v.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(v.Uint(), 10) - case reflect.Float32, reflect.Float64: + case reflect.Float32, reflect.Float64: return strconv.FormatFloat(v.Float(), 'f', -1, 64) case reflect.Bool: return strconv.FormatBool(v.Bool()) - case reflect.Slice: - if v.IsNil() { + case reflect.Slice: + if v.IsNil() { return "" } - if v.Type().Elem().Kind() == reflect.String { + if v.Type().Elem().Kind() == reflect.String { parts := make([]string, v.Len()) - for i := range parts { + for i := range parts { parts[i] = v.Index(i).String() } - return strings.Join(parts, ",") + return strings.Join(parts, ",") } return fmt.Sprint(v.Interface()) - case reflect.Ptr: + case reflect.Ptr: if v.IsNil() { return "(unset)" } - return stringifyValue(v.Elem()) + return stringifyValue(v.Elem()) default: return fmt.Sprint(v.Interface()) } @@ -7213,9 +7215,9 @@ import ( "golang.org/x/sys/unix" ) -func tryLockFile(fd uintptr) error { - if err := unix.Flock(int(fd), unix.LOCK_EX|unix.LOCK_NB); err != nil { - if errors.Is(err, unix.EWOULDBLOCK) { +func tryLockFile(fd uintptr) error { + if err := unix.Flock(int(fd), unix.LOCK_EX|unix.LOCK_NB); err != nil { + if errors.Is(err, unix.EWOULDBLOCK) { return errLockWouldBlock } return err @@ -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) { fd := f.Fd() - for { + for { err := tryLockFile(fd) if err == nil { return func() error { return unlockFile(fd) }, nil } - if errors.Is(err, errLockWouldBlock) { + if errors.Is(err, errLockWouldBlock) { select { case <-ctx.Done(): return nil, ctx.Err() - case <-time.After(5 * time.Millisecond): + case <-time.After(5 * time.Millisecond): } - continue + continue } return nil, err } @@ -7426,18 +7428,18 @@ func TakeSnapshot() (Snapshot, error) { } cutoff := time.Now().Add(-win) snap := Snapshot{Providers: make(map[string]ProviderEntry), Window: win} - for _, ev := range sf.Events { + for _, ev := range sf.Events { if ev.TS.Before(cutoff) { continue } - snap.Global.Reqs++ + snap.Global.Reqs++ snap.Global.Sent += ev.Sent snap.Global.Recv += ev.Recv pe := snap.Providers[ev.Provider] if pe.Models == nil { pe.Models = make(map[string]Counters) } - pe.Totals.Reqs++ + 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 { - if n < 1000 { + if n < 1000 { return fmt.Sprintf("%dB", n) } - const unit = 1000.0 + const unit = 1000.0 v := float64(n) suffix := []string{"k", "M", "G", "T"} i := 0 - for v >= unit && i < len(suffix)-1 { + for v >= unit && i < len(suffix)-1 { v /= unit i++ } - s := fmt.Sprintf("%.1f%s", v, suffix[i]) + s := fmt.Sprintf("%.1f%s", v, suffix[i]) // Strip trailing ".0" if len(s) >= 3 && s[len(s)-2:] == ".0" { s = fmt.Sprintf("%d%s", int(v), suffix[i]) } - return s + return s } -- cgit v1.2.3