summaryrefslogtreecommitdiff
path: root/docs/coverage.html
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-24 23:21:43 +0300
committerPaul Buetow <paul@buetow.org>2025-09-24 23:21:43 +0300
commitc3c71345db9086392cd9b7529c7f5287009c226e (patch)
treed227894ab900d6050cbe1418984526088a692db5 /docs/coverage.html
parent127844a4ee481590ef53b6777d34bf2114cb3ab1 (diff)
Add runtime config store and reload command
Diffstat (limited to 'docs/coverage.html')
-rw-r--r--docs/coverage.html2192
1 files changed, 1265 insertions, 927 deletions
diff --git a/docs/coverage.html b/docs/coverage.html
index 6828b9a..4a3153b 100644
--- a/docs/coverage.html
+++ b/docs/coverage.html
@@ -61,7 +61,7 @@
<option value="file2">codeberg.org/snonux/hexai/cmd/hexai/main.go (71.4%)</option>
- <option value="file3">codeberg.org/snonux/hexai/internal/appconfig/config.go (88.8%)</option>
+ <option value="file3">codeberg.org/snonux/hexai/internal/appconfig/config.go (88.9%)</option>
<option value="file4">codeberg.org/snonux/hexai/internal/editor/editor.go (58.3%)</option>
@@ -71,7 +71,7 @@
<option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (92.0%)</option>
- <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (71.0%)</option>
+ <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (76.8%)</option>
<option value="file9">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (65.5%)</option>
@@ -81,13 +81,13 @@
<option value="file12">codeberg.org/snonux/hexai/internal/hexaicli/run.go (90.0%)</option>
- <option value="file13">codeberg.org/snonux/hexai/internal/hexailsp/run.go (90.2%)</option>
+ <option value="file13">codeberg.org/snonux/hexai/internal/hexailsp/run.go (90.8%)</option>
<option value="file14">codeberg.org/snonux/hexai/internal/llm/copilot.go (82.4%)</option>
<option value="file15">codeberg.org/snonux/hexai/internal/llm/ollama.go (89.8%)</option>
- <option value="file16">codeberg.org/snonux/hexai/internal/llm/openai.go (87.1%)</option>
+ <option value="file16">codeberg.org/snonux/hexai/internal/llm/openai.go (86.4%)</option>
<option value="file17">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option>
@@ -99,41 +99,45 @@
<option value="file21">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option>
- <option value="file22">codeberg.org/snonux/hexai/internal/lsp/context.go (76.9%)</option>
+ <option value="file22">codeberg.org/snonux/hexai/internal/lsp/chat_commands.go (68.0%)</option>
- <option value="file23">codeberg.org/snonux/hexai/internal/lsp/document.go (91.5%)</option>
+ <option value="file23">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option>
- <option value="file24">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.9%)</option>
+ <option value="file24">codeberg.org/snonux/hexai/internal/lsp/document.go (91.5%)</option>
- <option value="file25">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (82.3%)</option>
+ <option value="file25">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.2%)</option>
- <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.2%)</option>
+ <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (84.1%)</option>
- <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (90.1%)</option>
+ <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_execute.go (75.0%)</option>
+ <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (87.6%)</option>
- <option value="file29">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (63.6%)</option>
+ <option value="file29">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option>
- <option value="file30">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (90.0%)</option>
+ <option value="file30">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (66.7%)</option>
- <option value="file31">codeberg.org/snonux/hexai/internal/lsp/server.go (79.8%)</option>
+ <option value="file31">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (89.9%)</option>
- <option value="file32">codeberg.org/snonux/hexai/internal/lsp/transport.go (73.0%)</option>
+ <option value="file32">codeberg.org/snonux/hexai/internal/lsp/server.go (85.4%)</option>
- <option value="file33">codeberg.org/snonux/hexai/internal/stats/lock_posix.go (83.3%)</option>
+ <option value="file33">codeberg.org/snonux/hexai/internal/lsp/transport.go (73.0%)</option>
- <option value="file34">codeberg.org/snonux/hexai/internal/stats/stats.go (75.8%)</option>
+ <option value="file34">codeberg.org/snonux/hexai/internal/runtimeconfig/store.go (85.5%)</option>
- <option value="file35">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option>
+ <option value="file35">codeberg.org/snonux/hexai/internal/stats/lock_posix.go (83.3%)</option>
- <option value="file36">codeberg.org/snonux/hexai/internal/textutil/human.go (92.3%)</option>
+ <option value="file36">codeberg.org/snonux/hexai/internal/stats/stats.go (75.8%)</option>
- <option value="file37">codeberg.org/snonux/hexai/internal/textutil/textutil.go (90.4%)</option>
+ <option value="file37">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option>
- <option value="file38">codeberg.org/snonux/hexai/internal/tmux/status.go (73.8%)</option>
+ <option value="file38">codeberg.org/snonux/hexai/internal/textutil/human.go (92.3%)</option>
- <option value="file39">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option>
+ <option value="file39">codeberg.org/snonux/hexai/internal/textutil/textutil.go (90.4%)</option>
+
+ <option value="file40">codeberg.org/snonux/hexai/internal/tmux/status.go (76.7%)</option>
+
+ <option value="file41">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option>
</select>
</div>
@@ -349,7 +353,7 @@ type CustomAction struct {
}
// Constructor: defaults for App (kept first among functions)
-func newDefaultConfig() App <span class="cov5" title="37">{
+func newDefaultConfig() App <span class="cov6" title="45">{
// Coding-friendly default temperature across providers
// Users can override per provider in config.toml (including 0.0).
t := 0.2
@@ -405,29 +409,40 @@ func newDefaultConfig() App <span class="cov5" title="37">{
// 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="cov5" title="36">{
+func Load(logger *log.Logger) App <span class="cov6" title="42">{ return LoadWithOptions(logger, LoadOptions{}) }</span>
+
+// LoadOptions tune how configuration is loaded at runtime.
+type LoadOptions struct {
+ // IgnoreEnv skips applying environment overrides when true.
+ IgnoreEnv bool
+}
+
+// LoadWithOptions reads configuration and applies the requested loading options.
+func LoadWithOptions(logger *log.Logger, opts LoadOptions) App <span class="cov6" title="44">{
cfg := newDefaultConfig()
- if logger == nil </span><span class="cov4" title="9">{
+ 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="27">configPath, err := getConfigPath()
+ <span class="cov5" title="31">configPath, err := getConfigPath()
if err != nil </span><span class="cov0" title="0">{
logger.Printf("%v", err)
- // Even if config path cannot be resolved, still allow env overrides below.
- }</span> else<span class="cov5" title="27"> {
- if fileCfg, err := loadFromFile(configPath, logger); err == nil &amp;&amp; fileCfg != nil </span><span class="cov5" title="22">{
+ // 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 &amp;&amp; fileCfg != nil </span><span class="cov5" title="26">{
cfg.mergeWith(fileCfg)
}</span>
// When the config file is missing or invalid, we keep defaults and still
- // apply any environment overrides below.
+ // apply any environment overrides below (unless disabled).
}
- // Environment overrides (take precedence over file)
- <span class="cov5" title="27">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov3" title="5">{
- cfg.mergeWith(envCfg)
- }</span>
- <span class="cov5" title="27">return cfg</span>
+ <span class="cov5" title="31">if !opts.IgnoreEnv </span><span class="cov5" title="29">{
+ // 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>
}
// Private helpers
@@ -496,7 +511,7 @@ type sectionOpenAI struct {
Presets map[string]string `toml:"presets"`
}
-func (s sectionOpenAI) isZero() bool <span class="cov5" title="22">{
+func (s sectionOpenAI) isZero() bool <span class="cov5" title="26">{
return strings.TrimSpace(s.Model) == "" &amp;&amp; strings.TrimSpace(s.BaseURL) == "" &amp;&amp; s.Temperature == nil &amp;&amp; len(s.Presets) == 0
}</span>
@@ -594,11 +609,11 @@ type sectionTmux struct {
CustomMenuHotkey string `toml:"custom_menu_hotkey"`
}
-func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
+func (fc *fileConfig) toApp() App <span class="cov5" title="26">{
out := App{}
// Merge section: general
- if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil </span><span class="cov2" title="3">{
+ if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil </span><span class="cov3" title="7">{
tmp := App{
MaxTokens: fc.General.MaxTokens,
ContextMode: fc.General.ContextMode,
@@ -610,13 +625,13 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
}</span>
// logging
- <span class="cov5" title="22">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{
+ <span class="cov5" title="26">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{
tmp := App{LogPreviewLimit: fc.Logging.LogPreviewLimit}
out.mergeBasics(&amp;tmp)
}</span>
// completion
- <span class="cov5" title="22">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{
+ <span class="cov5" title="26">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{
tmp := App{
CompletionDebounceMs: fc.Completion.CompletionDebounceMs,
CompletionThrottleMs: fc.Completion.CompletionThrottleMs,
@@ -626,31 +641,31 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
}</span>
// triggers
- <span class="cov5" title="22">if len(fc.Triggers.TriggerCharacters) &gt; 0 </span><span class="cov2" title="3">{
+ <span class="cov5" title="26">if len(fc.Triggers.TriggerCharacters) &gt; 0 </span><span class="cov2" title="3">{
tmp := App{TriggerCharacters: fc.Triggers.TriggerCharacters}
out.mergeBasics(&amp;tmp)
}</span>
// inline
- <span class="cov5" title="22">if (fc.Inline != sectionInline{}) </span><span class="cov1" title="1">{
+ <span class="cov5" title="26">if (fc.Inline != sectionInline{}) </span><span class="cov1" title="1">{
tmp := App{InlineOpen: fc.Inline.InlineOpen, InlineClose: fc.Inline.InlineClose}
out.mergeBasics(&amp;tmp)
}</span>
// chat
- <span class="cov5" title="22">if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) &gt; 0 </span><span class="cov1" title="1">{
+ <span class="cov5" title="26">if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) &gt; 0 </span><span class="cov1" title="1">{
tmp := App{ChatSuffix: fc.Chat.ChatSuffix, ChatPrefixes: fc.Chat.ChatPrefixes}
out.mergeBasics(&amp;tmp)
}</span>
// provider
- <span class="cov5" title="22">if strings.TrimSpace(fc.Provider.Name) != "" </span><span class="cov2" title="4">{
+ <span class="cov5" title="26">if strings.TrimSpace(fc.Provider.Name) != "" </span><span class="cov2" title="4">{
tmp := App{Provider: fc.Provider.Name}
out.mergeBasics(&amp;tmp)
}</span>
// openai
- <span class="cov5" title="22">if !fc.OpenAI.isZero() || fc.OpenAI.Temperature != nil </span><span class="cov2" title="4">{
+ <span class="cov5" title="26">if !fc.OpenAI.isZero() || fc.OpenAI.Temperature != nil </span><span class="cov2" title="4">{
tmp := App{
OpenAIBaseURL: fc.OpenAI.BaseURL,
OpenAIModel: fc.OpenAI.resolvedModel(),
@@ -660,7 +675,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
}</span>
// copilot
- <span class="cov5" title="22">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{
+ <span class="cov5" title="26">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{
tmp := App{
CopilotBaseURL: fc.Copilot.BaseURL,
CopilotModel: fc.Copilot.Model,
@@ -670,7 +685,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
}</span>
// ollama
- <span class="cov5" title="22">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{
+ <span class="cov5" title="26">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{
tmp := App{
OllamaBaseURL: fc.Ollama.BaseURL,
OllamaModel: fc.Ollama.Model,
@@ -681,7 +696,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
// prompts
// completion
- <span class="cov5" title="22">if (fc.Prompts.Completion != sectionPromptsCompletion{}) </span><span class="cov1" title="1">{
+ <span class="cov5" title="26">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>
@@ -702,11 +717,11 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
}</span>
}
// chat
- <span class="cov5" title="22">if strings.TrimSpace(fc.Prompts.Chat.System) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="26">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="22">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" ||
+ <span class="cov5" title="26">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) != "" ||
@@ -763,7 +778,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
}
}
// cli
- <span class="cov5" title="22">if (fc.Prompts.CLI != sectionPromptsCLI{}) </span><span class="cov1" title="1">{
+ <span class="cov5" title="26">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>
@@ -772,24 +787,24 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
}</span>
}
// provider-native
- <span class="cov5" title="22">if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="26">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="22">if (fc.Tmux != sectionTmux{}) </span><span class="cov2" title="3">{
+ <span class="cov5" title="26">if (fc.Tmux != sectionTmux{}) </span><span class="cov2" title="3">{
out.TmuxCustomMenuHotkey = strings.TrimSpace(fc.Tmux.CustomMenuHotkey)
}</span>
// stats
- <span class="cov5" title="22">if fc.Stats.WindowMinutes &gt; 0 </span><span class="cov0" title="0">{
+ <span class="cov5" title="26">if fc.Stats.WindowMinutes &gt; 0 </span><span class="cov0" title="0">{
out.StatsWindowMinutes = fc.Stats.WindowMinutes
}</span>
- <span class="cov5" title="22">return out</span>
+ <span class="cov5" title="26">return out</span>
}
-func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="28">{
+func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="32">{
b, err := os.ReadFile(path)
if err != nil </span><span class="cov2" title="4">{
if !os.IsNotExist(err) &amp;&amp; logger != nil </span><span class="cov0" title="0">{
@@ -798,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="24">var tables fileConfig
+ <span class="cov5" title="28">var tables fileConfig
errTables := toml.NewDecoder(strings.NewReader(string(b))).Decode(&amp;tables)
// Raw map for validation/presence checks
var raw map[string]any
@@ -811,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="22">legacy := map[string]struct{}{
+ <span class="cov5" title="26">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": {},
@@ -820,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="48">{
- if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable </span><span class="cov6" title="45">{
+ 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">{
continue</span>
}
<span class="cov2" title="3">if _, isLegacy := legacy[k]; isLegacy </span><span class="cov0" title="0">{
@@ -829,13 +844,13 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co
}</span>
}
- <span class="cov5" title="22">if logger != nil </span><span class="cov5" title="22">{
+ <span class="cov5" title="26">if logger != nil </span><span class="cov5" title="26">{
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="22">tab := tables.toApp()
+ <span class="cov5" title="26">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">{
@@ -849,7 +864,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co
}
}
}
- <span class="cov5" title="22">if t, ok := raw["logging"].(map[string]any); ok </span><span class="cov2" title="3">{
+ <span class="cov5" title="26">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">
@@ -861,142 +876,142 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co
}
}
}
- <span class="cov5" title="22">return &amp;tab, nil</span>
+ <span class="cov5" title="26">return &amp;tab, nil</span>
}
-func (a *App) mergeWith(other *App) <span class="cov5" title="27">{
+func (a *App) mergeWith(other *App) <span class="cov5" title="33">{
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="43">{
- if other.MaxTokens &gt; 0 </span><span class="cov3" title="7">{
+func (a *App) mergeBasics(other *App) <span class="cov6" title="53">{
+ if other.MaxTokens &gt; 0 </span><span class="cov4" title="17">{
a.MaxTokens = other.MaxTokens
}</span>
- <span class="cov6" title="43">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov3" title="7">{
+ <span class="cov6" title="53">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov3" title="7">{
a.ContextMode = s
}</span>
- <span class="cov6" title="43">if other.ContextWindowLines &gt; 0 </span><span class="cov3" title="7">{
+ <span class="cov6" title="53">if other.ContextWindowLines &gt; 0 </span><span class="cov3" title="7">{
a.ContextWindowLines = other.ContextWindowLines
}</span>
- <span class="cov6" title="43">if other.MaxContextTokens &gt; 0 </span><span class="cov3" title="7">{
+ <span class="cov6" title="53">if other.MaxContextTokens &gt; 0 </span><span class="cov3" title="7">{
a.MaxContextTokens = other.MaxContextTokens
}</span>
- <span class="cov6" title="43">if other.LogPreviewLimit &gt;= 0 </span><span class="cov6" title="43">{
+ <span class="cov6" title="53">if other.LogPreviewLimit &gt;= 0 </span><span class="cov6" title="53">{
a.LogPreviewLimit = other.LogPreviewLimit
}</span>
- <span class="cov6" title="43">if other.CodingTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0
+ <span class="cov6" title="53">if other.CodingTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0
a.CodingTemperature = other.CodingTemperature
}</span>
- <span class="cov6" title="43">if other.ManualInvokeMinPrefix &gt;= 0 </span><span class="cov6" title="43">{
+ <span class="cov6" title="53">if other.ManualInvokeMinPrefix &gt;= 0 </span><span class="cov6" title="53">{
a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix
}</span>
- <span class="cov6" title="43">if other.CompletionDebounceMs &gt; 0 </span><span class="cov3" title="7">{
+ <span class="cov6" title="53">if other.CompletionDebounceMs &gt; 0 </span><span class="cov3" title="7">{
a.CompletionDebounceMs = other.CompletionDebounceMs
}</span>
- <span class="cov6" title="43">if other.CompletionThrottleMs &gt; 0 </span><span class="cov3" title="7">{
+ <span class="cov6" title="53">if other.CompletionThrottleMs &gt; 0 </span><span class="cov3" title="7">{
a.CompletionThrottleMs = other.CompletionThrottleMs
}</span>
- <span class="cov6" title="43">if len(other.TriggerCharacters) &gt; 0 </span><span class="cov3" title="7">{
+ <span class="cov6" title="53">if len(other.TriggerCharacters) &gt; 0 </span><span class="cov3" title="7">{
a.TriggerCharacters = slices.Clone(other.TriggerCharacters)
}</span>
- <span class="cov6" title="43">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov1" title="2">{
+ <span class="cov6" title="53">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov1" title="2">{
a.InlineOpen = s
}</span>
- <span class="cov6" title="43">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov1" title="2">{
+ <span class="cov6" title="53">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov1" title="2">{
a.InlineClose = s
}</span>
- <span class="cov6" title="43">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov1" title="2">{
+ <span class="cov6" title="53">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov1" title="2">{
a.ChatSuffix = s
}</span>
- <span class="cov6" title="43">if len(other.ChatPrefixes) &gt; 0 </span><span class="cov1" title="2">{
+ <span class="cov6" title="53">if len(other.ChatPrefixes) &gt; 0 </span><span class="cov1" title="2">{
a.ChatPrefixes = slices.Clone(other.ChatPrefixes)
}</span>
- <span class="cov6" title="43">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov4" title="13">{
+ <span class="cov6" title="53">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="27">{
+func (a *App) mergePrompts(other *App) <span class="cov5" title="33">{
// Completion
if strings.TrimSpace(other.PromptCompletionSystemGeneral) != "" </span><span class="cov1" title="1">{
a.PromptCompletionSystemGeneral = other.PromptCompletionSystemGeneral
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCompletionSystemParams) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionSystemParams) != "" </span><span class="cov1" title="1">{
a.PromptCompletionSystemParams = other.PromptCompletionSystemParams
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCompletionSystemInline) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionSystemInline) != "" </span><span class="cov1" title="1">{
a.PromptCompletionSystemInline = other.PromptCompletionSystemInline
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCompletionUserGeneral) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionUserGeneral) != "" </span><span class="cov1" title="1">{
a.PromptCompletionUserGeneral = other.PromptCompletionUserGeneral
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCompletionUserParams) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionUserParams) != "" </span><span class="cov1" title="1">{
a.PromptCompletionUserParams = other.PromptCompletionUserParams
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCompletionExtraHeader) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCompletionExtraHeader) != "" </span><span class="cov1" title="1">{
a.PromptCompletionExtraHeader = other.PromptCompletionExtraHeader
}</span>
// Provider-native
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptNativeCompletion) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptNativeCompletion) != "" </span><span class="cov1" title="1">{
a.PromptNativeCompletion = other.PromptNativeCompletion
}</span>
// Chat
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptChatSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptChatSystem) != "" </span><span class="cov1" title="1">{
a.PromptChatSystem = other.PromptChatSystem
}</span>
// Code actions
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionRewriteSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionRewriteSystem) != "" </span><span class="cov1" title="1">{
a.PromptCodeActionRewriteSystem = other.PromptCodeActionRewriteSystem
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionDiagnosticsSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionDiagnosticsSystem) != "" </span><span class="cov1" title="1">{
a.PromptCodeActionDiagnosticsSystem = other.PromptCodeActionDiagnosticsSystem
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionDocumentSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionDocumentSystem) != "" </span><span class="cov1" title="1">{
a.PromptCodeActionDocumentSystem = other.PromptCodeActionDocumentSystem
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionRewriteUser) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionRewriteUser) != "" </span><span class="cov1" title="1">{
a.PromptCodeActionRewriteUser = other.PromptCodeActionRewriteUser
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionDiagnosticsUser) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionDiagnosticsUser) != "" </span><span class="cov1" title="1">{
a.PromptCodeActionDiagnosticsUser = other.PromptCodeActionDiagnosticsUser
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionDocumentUser) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionDocumentUser) != "" </span><span class="cov1" title="1">{
a.PromptCodeActionDocumentUser = other.PromptCodeActionDocumentUser
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" </span><span class="cov1" title="1">{
a.PromptCodeActionGoTestSystem = other.PromptCodeActionGoTestSystem
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" </span><span class="cov1" title="1">{
a.PromptCodeActionGoTestUser = other.PromptCodeActionGoTestUser
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionSimplifySystem) != "" </span><span class="cov0" title="0">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionSimplifySystem) != "" </span><span class="cov0" title="0">{
a.PromptCodeActionSimplifySystem = other.PromptCodeActionSimplifySystem
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCodeActionSimplifyUser) != "" </span><span class="cov0" title="0">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCodeActionSimplifyUser) != "" </span><span class="cov0" title="0">{
a.PromptCodeActionSimplifyUser = other.PromptCodeActionSimplifyUser
}</span>
// CLI
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" </span><span class="cov1" title="1">{
a.PromptCLIDefaultSystem = other.PromptCLIDefaultSystem
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.PromptCLIExplainSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="33">if strings.TrimSpace(other.PromptCLIExplainSystem) != "" </span><span class="cov1" title="1">{
a.PromptCLIExplainSystem = other.PromptCLIExplainSystem
}</span>
// Custom actions
- <span class="cov5" title="27">if len(other.CustomActions) &gt; 0 </span><span class="cov4" title="16">{
+ <span class="cov5" title="33">if len(other.CustomActions) &gt; 0 </span><span class="cov4" title="16">{
a.CustomActions = append([]CustomAction{}, other.CustomActions...)
}</span>
- <span class="cov5" title="27">if strings.TrimSpace(other.TmuxCustomMenuHotkey) != "" </span><span class="cov2" title="3">{
+ <span class="cov5" title="33">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="19">{
+func (a App) Validate() error <span class="cov5" title="22">{
// Normalize and check duplicates for IDs and hotkeys
seenID := make(map[string]struct{})
seenHK := make(map[string]struct{})
@@ -1039,7 +1054,7 @@ func (a App) Validate() error <span class="cov5" title="19">{
}
}
// Tmux custom menu hotkey validation
- <span class="cov4" title="14">if hk := strings.TrimSpace(a.TmuxCustomMenuHotkey); hk != "" </span><span class="cov1" title="2">{
+ <span class="cov4" title="17">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>
@@ -1049,43 +1064,43 @@ func (a App) Validate() error <span class="cov5" title="19">{
return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s (clashes with built-in)", hk)</span>
}
}
- <span class="cov4" title="13">return nil</span>
+ <span class="cov4" title="16">return nil</span>
}
// mergeProviderFields merges per-provider configuration.
-func (a *App) mergeProviderFields(other *App) <span class="cov5" title="37">{
+func (a *App) mergeProviderFields(other *App) <span class="cov6" title="43">{
if s := strings.TrimSpace(other.OpenAIBaseURL); s != "" </span><span class="cov3" title="7">{
a.OpenAIBaseURL = s
}</span>
- <span class="cov5" title="37">if s := strings.TrimSpace(other.OpenAIModel); s != "" </span><span class="cov4" title="13">{
+ <span class="cov6" title="43">if s := strings.TrimSpace(other.OpenAIModel); s != "" </span><span class="cov4" title="13">{
a.OpenAIModel = s
}</span>
- <span class="cov5" title="37">if other.OpenAITemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0
+ <span class="cov6" title="43">if other.OpenAITemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0
a.OpenAITemperature = other.OpenAITemperature
}</span>
- <span class="cov5" title="37">if s := strings.TrimSpace(other.OllamaBaseURL); s != "" </span><span class="cov3" title="7">{
+ <span class="cov6" title="43">if s := strings.TrimSpace(other.OllamaBaseURL); s != "" </span><span class="cov3" title="7">{
a.OllamaBaseURL = s
}</span>
- <span class="cov5" title="37">if s := strings.TrimSpace(other.OllamaModel); s != "" </span><span class="cov3" title="7">{
+ <span class="cov6" title="43">if s := strings.TrimSpace(other.OllamaModel); s != "" </span><span class="cov3" title="7">{
a.OllamaModel = s
}</span>
- <span class="cov5" title="37">if other.OllamaTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0
+ <span class="cov6" title="43">if other.OllamaTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0
a.OllamaTemperature = other.OllamaTemperature
}</span>
- <span class="cov5" title="37">if s := strings.TrimSpace(other.CopilotBaseURL); s != "" </span><span class="cov3" title="7">{
+ <span class="cov6" title="43">if s := strings.TrimSpace(other.CopilotBaseURL); s != "" </span><span class="cov3" title="7">{
a.CopilotBaseURL = s
}</span>
- <span class="cov5" title="37">if s := strings.TrimSpace(other.CopilotModel); s != "" </span><span class="cov3" title="7">{
+ <span class="cov6" title="43">if s := strings.TrimSpace(other.CopilotModel); s != "" </span><span class="cov3" title="7">{
a.CopilotModel = s
}</span>
- <span class="cov5" title="37">if other.CopilotTemperature != nil </span><span class="cov3" title="7">{ // allow explicit 0.0
+ <span class="cov6" title="43">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="28">{
+func getConfigPath() (string, error) <span class="cov5" title="32">{
var configPath string
- if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov4" title="18">{
+ if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov5" title="22">{
configPath = filepath.Join(xdgConfigHome, "hexai", "config.toml")
}</span> else<span class="cov4" title="10"> {
home, err := os.UserHomeDir()
@@ -1094,36 +1109,36 @@ func getConfigPath() (string, error) <span class="cov5" title="28">{
}</span>
<span class="cov4" title="10">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span>
}
- <span class="cov5" title="28">return configPath, nil</span>
+ <span class="cov5" title="32">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="27">{
+func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="29">{
var out App
var any bool
// helpers
- getenv := func(k string) string </span><span class="cov10" title="702">{ return strings.TrimSpace(os.Getenv(k)) }</span>
- <span class="cov5" title="27">parseInt := func(k string) (int, bool) </span><span class="cov8" title="189">{
+ 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">{
v := getenv(k)
- if v == "" </span><span class="cov8" title="182">{
+ if v == "" </span><span class="cov8" title="194">{
return 0, false
}</span>
- <span class="cov3" title="7">n, err := strconv.Atoi(v)
+ <span class="cov3" title="9">n, err := strconv.Atoi(v)
if err != nil </span><span class="cov0" title="0">{
if logger != nil </span><span class="cov0" title="0">{
logger.Printf("invalid %s: %v", k, err)
}</span>
<span class="cov0" title="0">return 0, false</span>
}
- <span class="cov3" title="7">return n, true</span>
+ <span class="cov3" title="9">return n, true</span>
}
- <span class="cov5" title="27">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="108">{
+ <span class="cov5" title="29">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="116">{
v := getenv(k)
- if v == "" </span><span class="cov7" title="104">{
+ if v == "" </span><span class="cov7" title="112">{
return nil, false
}</span>
<span class="cov2" title="4">f, err := strconv.ParseFloat(v, 64)
@@ -1136,43 +1151,43 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="27">{
<span class="cov2" title="4">return &amp;f, true</span>
}
- <span class="cov5" title="27">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov2" title="3">{
out.MaxTokens = n
any = true
}</span>
- <span class="cov5" title="27">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{
out.ContextMode = s
any = true
}</span>
- <span class="cov5" title="27">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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="27">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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="27">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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="27">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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="27">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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="27">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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="27">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{
out.CodingTemperature = f
any = true
}</span>
- <span class="cov5" title="27">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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">{
@@ -1182,19 +1197,19 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="27">{
}
<span class="cov1" title="1">any = true</span>
}
- <span class="cov5" title="27">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{
+ <span class="cov5" title="29">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{
out.InlineOpen = s
any = true
}</span>
- <span class="cov5" title="27">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{
+ <span class="cov5" title="29">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{
out.InlineClose = s
any = true
}</span>
- <span class="cov5" title="27">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{
+ <span class="cov5" title="29">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{
out.ChatSuffix = s
any = true
}</span>
- <span class="cov5" title="27">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{
+ <span class="cov5" title="29">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">{
@@ -1204,17 +1219,17 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="27">{
}
<span class="cov0" title="0">any = true</span>
}
- <span class="cov5" title="27">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov3" title="5">{
+ <span class="cov5" title="29">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov3" title="5">{
out.Provider = s
any = true
}</span>
- <span class="cov5" title="27">modelForce := strings.TrimSpace(getenv("HEXAI_MODEL_FORCE"))
+ <span class="cov5" title="29">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="81">{
+ pickModel := func(providerName, specific string) (string, bool) </span><span class="cov7" title="87">{
specific = strings.TrimSpace(specific)
nameLower := strings.ToLower(strings.TrimSpace(providerName))
if modelForce != "" </span><span class="cov2" title="3">{
@@ -1227,10 +1242,10 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="27">{
return modelForce, true
}</span>
}
- <span class="cov7" title="80">if specific != "" </span><span class="cov2" title="4">{
+ <span class="cov7" title="86">if specific != "" </span><span class="cov2" title="4">{
return specific, true
}</span>
- <span class="cov6" title="76">if modelGeneric != "" </span><span class="cov3" title="8">{
+ <span class="cov6" title="82">if modelGeneric != "" </span><span class="cov3" title="8">{
if providerLower == nameLower </span><span class="cov1" title="2">{
return modelGeneric, true
}</span>
@@ -1239,53 +1254,53 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="27">{
return modelGeneric, true
}</span>
}
- <span class="cov6" title="74">return "", false</span>
+ <span class="cov6" title="80">return "", false</span>
}
// Provider-specific
- <span class="cov5" title="27">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{
out.OpenAIBaseURL = s
any = true
}</span>
- <span class="cov5" title="27">if model, ok := pickModel("openai", getenv("HEXAI_OPENAI_MODEL")); ok </span><span class="cov3" title="5">{
+ <span class="cov5" title="29">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="27">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{
out.OpenAITemperature = f
any = true
}</span>
- <span class="cov5" title="27">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{
out.OllamaBaseURL = s
any = true
}</span>
- <span class="cov5" title="27">if model, ok := pickModel("ollama", getenv("HEXAI_OLLAMA_MODEL")); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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="27">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{
out.OllamaTemperature = f
any = true
}</span>
- <span class="cov5" title="27">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{
out.CopilotBaseURL = s
any = true
}</span>
- <span class="cov5" title="27">if model, ok := pickModel("copilot", getenv("HEXAI_COPILOT_MODEL")); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">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="27">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <span class="cov5" title="29">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{
out.CopilotTemperature = f
any = true
}</span>
- <span class="cov5" title="27">if !any </span><span class="cov5" title="22">{
+ <span class="cov5" title="29">if !any </span><span class="cov5" title="22">{
return nil
}</span>
- <span class="cov3" title="5">return &amp;out</span>
+ <span class="cov3" title="7">return &amp;out</span>
}
</pre>
@@ -1632,10 +1647,10 @@ import (
)
// Render performs simple {{var}} replacement like LSP.
-func Render(t string, vars map[string]string) string <span class="cov7" title="15">{ return textutil.RenderTemplate(t, vars) }</span>
+func Render(t string, vars map[string]string) string <span class="cov7" title="18">{ return textutil.RenderTemplate(t, vars) }</span>
// StripFences removes surrounding markdown code fences.
-func StripFences(s string) string <span class="cov7" title="16">{ return textutil.StripCodeFences(s) }</span>
+func StripFences(s string) string <span class="cov7" title="19">{ return textutil.StripCodeFences(s) }</span>
type chatDoer interface {
Chat(ctx context.Context, msgs []llm.Message, opts ...llm.RequestOption) (string, error)
@@ -1644,31 +1659,31 @@ type chatDoer interface {
type providerNamer interface{ Name() string }
-func providerOf(c any) string <span class="cov10" title="45">{
+func providerOf(c any) string <span class="cov10" title="54">{
if n, ok := c.(providerNamer); ok </span><span class="cov5" title="6">{
return n.Name()
}</span>
- <span class="cov9" title="39">return "llm"</span>
+ <span class="cov9" title="48">return "llm"</span>
}
-func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruction, selection string) (string, error) <span class="cov5" title="6">{
+func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruction, selection string) (string, error) <span class="cov5" title="7">{
sys := cfg.PromptCodeActionRewriteSystem
user := Render(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": instruction, "selection": selection})
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))
}</span>
-func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, diags []string, selection string) (string, error) <span class="cov1" title="1">{
+func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, diags []string, selection string) (string, error) <span class="cov2" title="2">{
var b strings.Builder
- for i, d := range diags </span><span class="cov2" title="2">{
+ for i, d := range diags </span><span class="cov3" title="3">{
if strings.TrimSpace(d) == "" </span><span class="cov0" title="0">{
continue</span>
}
- <span class="cov2" title="2">b.WriteString(strings.TrimSpace(d))
+ <span class="cov3" title="3">b.WriteString(strings.TrimSpace(d))
if i &lt; len(diags)-1 </span><span class="cov1" title="1">{
b.WriteString("\n")
}</span>
}
- <span class="cov1" title="1">sys := cfg.PromptCodeActionDiagnosticsSystem
+ <span class="cov2" title="2">sys := cfg.PromptCodeActionDiagnosticsSystem
user := Render(cfg.PromptCodeActionDiagnosticsUser, map[string]string{"diagnostics": b.String(), "selection": selection})
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))</span>
}
@@ -1679,7 +1694,7 @@ func runDocument(ctx context.Context, cfg appconfig.App, client chatDoer, select
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))
}</span>
-func runSimplify(ctx context.Context, cfg appconfig.App, client chatDoer, selection string) (string, error) <span class="cov1" title="1">{
+func runSimplify(ctx context.Context, cfg appconfig.App, client chatDoer, selection string) (string, error) <span class="cov2" title="2">{
sys := cfg.PromptCodeActionSimplifySystem
user := Render(cfg.PromptCodeActionSimplifyUser, map[string]string{"selection": selection})
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))
@@ -1691,7 +1706,7 @@ func runGoTest(ctx context.Context, cfg appconfig.App, client chatDoer, funcCode
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))
}</span>
-func runCustom(ctx context.Context, cfg appconfig.App, client chatDoer, ca appconfig.CustomAction, parts InputParts) (string, error) <span class="cov4" title="4">{
+func runCustom(ctx context.Context, cfg appconfig.App, client chatDoer, ca appconfig.CustomAction, parts InputParts) (string, error) <span class="cov4" title="5">{
// If user template is provided, prefer it and optional system
if strings.TrimSpace(ca.User) != "" </span><span class="cov2" title="2">{
sys := cfg.PromptCodeActionRewriteSystem
@@ -1703,7 +1718,7 @@ func runCustom(ctx context.Context, cfg appconfig.App, client chatDoer, ca appco
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))</span>
}
// Else, use fixed instruction through rewrite template
- <span class="cov2" title="2">return runRewrite(ctx, cfg, client, ca.Instruction, parts.Selection)</span>
+ <span class="cov3" title="3">return runRewrite(ctx, cfg, client, ca.Instruction, parts.Selection)</span>
}
func runOnce(ctx context.Context, client chatDoer, sys, user string) (string, error) <span class="cov1" title="1">{
@@ -1737,55 +1752,55 @@ func runOnce(ctx context.Context, client chatDoer, sys, user string) (string, er
<span class="cov1" title="1">return out, nil</span>
}
-func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) <span class="cov7" title="14">{
+func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) <span class="cov7" title="17">{
msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
txt, err := client.Chat(ctx, msgs, opts...)
if err != nil </span><span class="cov0" title="0">{
return "", err
}</span>
- <span class="cov7" title="14">out := strings.TrimSpace(StripFences(txt))
+ <span class="cov7" title="17">out := strings.TrimSpace(StripFences(txt))
// Contribute to global stats and update tmux status
sent := 0
- for _, m := range msgs </span><span class="cov8" title="28">{
+ for _, m := range msgs </span><span class="cov8" title="34">{
sent += len(m.Content)
}</span>
- <span class="cov7" title="14">recv := len(out)
+ <span class="cov7" title="17">recv := len(out)
_ = stats.Update(ctx, providerOf(client), client.DefaultModel(), sent, recv)
- if snap, err := stats.TakeSnapshot(); err == nil </span><span class="cov7" title="14">{
+ if snap, err := stats.TakeSnapshot(); err == nil </span><span class="cov7" title="17">{
minsWin := snap.Window.Minutes()
if minsWin &lt;= 0 </span><span class="cov0" title="0">{
minsWin = 0.001
}</span>
- <span class="cov7" title="14">scopeReqs := int64(0)
- if pe, ok := snap.Providers[providerOf(client)]; ok </span><span class="cov7" title="14">{
- if mc, ok2 := pe.Models[client.DefaultModel()]; ok2 </span><span class="cov7" title="14">{
+ <span class="cov7" title="17">scopeReqs := int64(0)
+ if pe, ok := snap.Providers[providerOf(client)]; ok </span><span class="cov7" title="17">{
+ if mc, ok2 := pe.Models[client.DefaultModel()]; ok2 </span><span class="cov7" title="17">{
scopeReqs = mc.Reqs
}</span>
}
- <span class="cov7" title="14">scopeRPM := float64(scopeReqs) / minsWin
+ <span class="cov7" title="17">scopeRPM := float64(scopeReqs) / minsWin
_ = tmux.SetStatus(tmux.FormatGlobalStatusColored(snap.Global.Reqs, snap.RPM, snap.Global.Sent, snap.Global.Recv, providerOf(client), client.DefaultModel(), scopeRPM, scopeReqs, snap.Window))</span>
}
- <span class="cov7" title="14">return out, nil</span>
+ <span class="cov7" title="17">return out, nil</span>
}
// reqOptsFrom builds LLM request options similar to LSP behavior.
-func reqOptsFrom(cfg appconfig.App) []llm.RequestOption <span class="cov7" title="14">{
+func reqOptsFrom(cfg appconfig.App) []llm.RequestOption <span class="cov7" title="17">{
opts := []llm.RequestOption{llm.WithMaxTokens(cfg.MaxTokens)}
// Apply temperature, with special-case for gpt-5 (default temp must be 1.0)
- if cfg.CodingTemperature != nil </span><span class="cov6" title="10">{
+ if cfg.CodingTemperature != nil </span><span class="cov6" title="13">{
temp := *cfg.CodingTemperature
prov := strings.ToLower(strings.TrimSpace(cfg.Provider))
model := strings.ToLower(strings.TrimSpace(cfg.OpenAIModel))
if prov == "openai" &amp;&amp; strings.HasPrefix(model, "gpt-5") </span><span class="cov0" title="0">{
temp = 1.0
}</span>
- <span class="cov6" title="10">opts = append(opts, llm.WithTemperature(temp))</span>
+ <span class="cov6" title="13">opts = append(opts, llm.WithTemperature(temp))</span>
}
- <span class="cov7" title="14">return opts</span>
+ <span class="cov7" title="17">return opts</span>
}
// Timeout helpers to mirror LSP behavior.
-func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov5" title="7">{
+func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov6" title="10">{
return context.WithTimeout(parent, 20*time.Second)
}</span>
@@ -1864,7 +1879,7 @@ func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error <
return nil</span>
}
-func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) <span class="cov10" title="10">{
+func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) <span class="cov9" title="10">{
switch kind </span>{
case ActionSkip:<span class="cov3" title="2">
return parts.Selection, nil</span>
@@ -1898,8 +1913,8 @@ func handleRewriteAction(ctx context.Context, parts InputParts, cfg appconfig.Ap
}</span>)
}
-func handleDiagnosticsAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) <span class="cov0" title="0">{
- return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) </span><span class="cov0" title="0">{
+func handleDiagnosticsAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) <span class="cov1" title="1">{
+ return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) </span><span class="cov1" title="1">{
return runDiagnostics(cctx, cfg, client, parts.Diagnostics, parts.Selection)
}</span>)
}
@@ -1916,17 +1931,17 @@ func handleGoTestAction(ctx context.Context, parts InputParts, cfg appconfig.App
}</span>)
}
-func handleSimplifyAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) <span class="cov0" title="0">{
- return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) </span><span class="cov0" title="0">{
+func handleSimplifyAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) <span class="cov1" title="1">{
+ return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) </span><span class="cov1" title="1">{
return runSimplify(cctx, cfg, client, parts.Selection)
}</span>)
}
-func handleCustomAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) <span class="cov5" title="3">{
+func handleCustomAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) <span class="cov6" title="4">{
if selectedCustom == nil </span><span class="cov0" title="0">{
return parts.Selection, nil
}</span>
- <span class="cov5" title="3">return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) </span><span class="cov5" title="3">{
+ <span class="cov6" title="4">return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) </span><span class="cov6" title="4">{
out, err := runCustom(cctx, cfg, client, *selectedCustom, parts)
selectedCustom = nil
return out, err
@@ -1944,7 +1959,7 @@ func handleCustomPromptAction(ctx context.Context, parts InputParts, cfg appconf
}</span>)
}
-func runWithTimeout(ctx context.Context, timeout func(context.Context) (context.Context, context.CancelFunc), fn func(context.Context) (string, error)) (string, error) <span class="cov9" title="8">{
+func runWithTimeout(ctx context.Context, timeout func(context.Context) (context.Context, context.CancelFunc), fn func(context.Context) (string, error)) (string, error) <span class="cov10" title="11">{
innerCtx, cancel := timeout(ctx)
defer cancel()
return fn(innerCtx)
@@ -2400,6 +2415,7 @@ import (
"codeberg.org/snonux/hexai/internal/llm"
"codeberg.org/snonux/hexai/internal/logging"
"codeberg.org/snonux/hexai/internal/lsp"
+ "codeberg.org/snonux/hexai/internal/runtimeconfig"
"codeberg.org/snonux/hexai/internal/stats"
)
@@ -2434,36 +2450,54 @@ func Run(logPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) er
// RunWithFactory is the testable entrypoint. When client is nil, it is built from cfg+env.
// When factory is nil, lsp.NewServer is used.
-func RunWithFactory(logPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory) error <span class="cov10" title="7">{
+func RunWithFactory(logPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory) error <span class="cov9" title="8">{
normalizeLoggingConfig(&amp;cfg)
if err := cfg.Validate(); err != nil </span><span class="cov0" title="0">{
logger.Fatalf("invalid config: %v", err)
}</span>
- <span class="cov10" title="7">client = buildClientIfNil(cfg, client)
+ <span class="cov9" title="8">client = buildClientIfNil(cfg, client)
factory = ensureFactory(factory)
- opts := makeServerOptions(cfg, strings.TrimSpace(logPath) != "", client)
+ store := runtimeconfig.New(cfg)
+ logContext := strings.TrimSpace(logPath) != ""
+ opts := makeServerOptions(cfg, logContext, client)
+ opts.ConfigStore = store
server := factory(stdin, stdout, logger, opts)
- if err := server.Run(); err != nil </span><span class="cov0" title="0">{
+ if configurable, ok := server.(interface{ ApplyOptions(lsp.ServerOptions) }); ok </span><span class="cov3" title="2">{
+ store.Subscribe(func(oldCfg, newCfg appconfig.App) </span><span class="cov1" title="1">{
+ updated := newCfg
+ normalizeLoggingConfig(&amp;updated)
+ if updated.StatsWindowMinutes &gt; 0 </span><span class="cov0" title="0">{
+ stats.SetWindow(time.Duration(updated.StatsWindowMinutes) * time.Minute)
+ }</span>
+ <span class="cov1" title="1">if newClient := buildClientIfNil(updated, nil); newClient != nil </span><span class="cov0" title="0">{
+ client = newClient
+ }</span>
+ <span class="cov1" title="1">opts := makeServerOptions(updated, logContext, client)
+ opts.ConfigStore = store
+ configurable.ApplyOptions(opts)</span>
+ })
+ }
+ <span class="cov9" title="8">if err := server.Run(); err != nil </span><span class="cov0" title="0">{
logger.Fatalf("server error: %v", err)
}</span>
- <span class="cov10" title="7">return nil</span>
+ <span class="cov9" title="8">return nil</span>
}
// --- helpers to keep RunWithFactory small ---
-func normalizeLoggingConfig(cfg *appconfig.App) <span class="cov10" title="7">{
+func normalizeLoggingConfig(cfg *appconfig.App) <span class="cov10" title="9">{
cfg.ContextMode = strings.ToLower(strings.TrimSpace(cfg.ContextMode))
- if cfg.LogPreviewLimit &gt;= 0 </span><span class="cov10" title="7">{
+ if cfg.LogPreviewLimit &gt;= 0 </span><span class="cov10" title="9">{
logging.SetLogPreviewLimit(cfg.LogPreviewLimit)
}</span>
}
-func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client <span class="cov10" title="7">{
- if client != nil </span><span class="cov0" title="0">{
+func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client <span class="cov10" title="9">{
+ if client != nil </span><span class="cov1" title="1">{
return client
}</span>
- <span class="cov10" title="7">llmCfg := llm.Config{
+ <span class="cov9" title="8">llmCfg := llm.Config{
Provider: cfg.Provider,
OpenAIBaseURL: cfg.OpenAIBaseURL,
OpenAIModel: cfg.OpenAIModel,
@@ -2477,25 +2511,25 @@ func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client <span cla
}
// Prefer HEXAI_OPENAI_API_KEY; fall back to OPENAI_API_KEY
oaKey := os.Getenv("HEXAI_OPENAI_API_KEY")
- if strings.TrimSpace(oaKey) == "" </span><span class="cov10" title="7">{
+ if strings.TrimSpace(oaKey) == "" </span><span class="cov9" title="8">{
oaKey = os.Getenv("OPENAI_API_KEY")
}</span>
// Prefer HEXAI_COPILOT_API_KEY; fall back to COPILOT_API_KEY
- <span class="cov10" title="7">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY")
- if strings.TrimSpace(cpKey) == "" </span><span class="cov10" title="7">{
+ <span class="cov9" title="8">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY")
+ if strings.TrimSpace(cpKey) == "" </span><span class="cov9" title="8">{
cpKey = os.Getenv("COPILOT_API_KEY")
}</span>
- <span class="cov10" title="7">if c, err := llm.NewFromConfig(llmCfg, oaKey, cpKey); err != nil </span><span class="cov1" title="1">{
+ <span class="cov9" title="8">if c, err := llm.NewFromConfig(llmCfg, oaKey, cpKey); err != nil </span><span class="cov8" title="6">{
logging.Logf("lsp ", "llm disabled: %v", err)
return nil
- }</span> else<span class="cov9" title="6"> {
+ }</span> else<span class="cov3" title="2"> {
logging.Logf("lsp ", "llm enabled provider=%s model=%s", c.Name(), c.DefaultModel())
return c
}</span>
}
-func ensureFactory(factory ServerFactory) ServerFactory <span class="cov10" title="7">{
- if factory != nil </span><span class="cov9" title="6">{
+func ensureFactory(factory ServerFactory) ServerFactory <span class="cov9" title="8">{
+ if factory != nil </span><span class="cov8" title="7">{
return factory
}</span>
<span class="cov1" title="1">return func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner </span><span class="cov1" title="1">{
@@ -2503,12 +2537,12 @@ func ensureFactory(factory ServerFactory) ServerFactory <span class="cov10" titl
}</span>
}
-func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) lsp.ServerOptions <span class="cov10" title="7">{
+func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) lsp.ServerOptions <span class="cov10" title="9">{
// Map custom actions from appconfig to lsp type
var customs []lsp.CustomAction
- if len(cfg.CustomActions) &gt; 0 </span><span class="cov4" title="2">{
+ if len(cfg.CustomActions) &gt; 0 </span><span class="cov3" title="2">{
customs = make([]lsp.CustomAction, 0, len(cfg.CustomActions))
- for _, ca := range cfg.CustomActions </span><span class="cov7" title="4">{
+ for _, ca := range cfg.CustomActions </span><span class="cov6" title="4">{
customs = append(customs, lsp.CustomAction{
ID: ca.ID,
Title: ca.Title,
@@ -2520,8 +2554,10 @@ func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) ls
})
}</span>
}
- <span class="cov10" title="7">return lsp.ServerOptions{
+ <span class="cov10" title="9">return lsp.ServerOptions{
LogContext: logContext,
+ ConfigStore: nil,
+ Config: &amp;cfg,
MaxTokens: cfg.MaxTokens,
ContextMode: cfg.ContextMode,
WindowLines: cfg.ContextWindowLines,
@@ -3004,7 +3040,7 @@ func newOllama(baseURL, model string, defaultTemp *float64) Client <span class="
baseURL = "http://localhost:11434"
}</span>
<span class="cov10" title="13">if strings.TrimSpace(model) == "" </span><span class="cov3" title="2">{
- model = "qwen3-coder:30b-a3b-q4_K_M`"
+ model = "qwen3-coder:30b-a3b-q4_K_M"
}</span>
<span class="cov10" title="13">return ollamaClient{
httpClient: &amp;http.Client{Timeout: 30 * time.Second},
@@ -3252,14 +3288,14 @@ type oaStreamChunk struct {
// Constructor (kept among the first functions by convention)
// newOpenAI constructs an OpenAI client using explicit configuration values.
// The apiKey may be empty; calls will fail until a valid key is supplied.
-func newOpenAI(baseURL, model, apiKey string, defaultTemp *float64) Client <span class="cov10" title="20">{
- if strings.TrimSpace(baseURL) == "" </span><span class="cov7" title="9">{
+func newOpenAI(baseURL, model, apiKey string, defaultTemp *float64) Client <span class="cov9" title="16">{
+ if strings.TrimSpace(baseURL) == "" </span><span class="cov6" title="5">{
baseURL = "https://api.openai.com/v1"
}</span>
- <span class="cov10" title="20">if strings.TrimSpace(model) == "" </span><span class="cov6" title="6">{
+ <span class="cov9" title="16">if strings.TrimSpace(model) == "" </span><span class="cov3" title="2">{
model = "gpt-4.1"
}</span>
- <span class="cov10" title="20">return openAIClient{
+ <span class="cov9" title="16">return openAIClient{
httpClient: &amp;http.Client{Timeout: 30 * time.Second},
apiKey: apiKey,
baseURL: baseURL,
@@ -3269,18 +3305,18 @@ func newOpenAI(baseURL, model, apiKey string, defaultTemp *float64) Client <span
}</span>
}
-func (c openAIClient) Chat(ctx context.Context, messages []Message, opts ...RequestOption) (string, error) <span class="cov6" title="6">{
+func (c openAIClient) Chat(ctx context.Context, messages []Message, opts ...RequestOption) (string, error) <span class="cov7" title="8">{
if c.apiKey == "" </span><span class="cov1" title="1">{
return nilStringErr("missing OpenAI API key")
}</span>
- <span class="cov5" title="5">o := Options{Model: c.defaultModel}
+ <span class="cov7" title="7">o := Options{Model: c.defaultModel}
for _, opt := range opts </span><span class="cov0" title="0">{
opt(&amp;o)
}</span>
- <span class="cov5" title="5">if o.Model == "" </span><span class="cov0" title="0">{
+ <span class="cov7" title="7">if o.Model == "" </span><span class="cov0" title="0">{
o.Model = c.defaultModel
}</span>
- <span class="cov5" title="5">start := time.Now()
+ <span class="cov7" title="7">start := time.Now()
c.logStart(false, o, messages)
req := buildOAChatRequest(o, messages, c.defaultTemperature, false)
body, err := json.Marshal(req)
@@ -3288,7 +3324,7 @@ func (c openAIClient) Chat(ctx context.Context, messages []Message, opts ...Requ
c.logf("marshal error: %v", err)
return "", err
}</span>
- <span class="cov5" title="5">endpoint := c.baseURL + "/chat/completions"
+ <span class="cov7" title="7">endpoint := c.baseURL + "/chat/completions"
logging.Logf("llm/openai ", "POST %s", endpoint)
resp, err := c.doJSON(ctx, endpoint, body, map[string]string{
"Authorization": "Bearer " + c.apiKey,
@@ -3297,41 +3333,41 @@ func (c openAIClient) Chat(ctx context.Context, messages []Message, opts ...Requ
logging.Logf("llm/openai ", "%shttp error after %s: %v%s", logging.AnsiRed, time.Since(start), err, logging.AnsiBase)
return "", err
}</span>
- <span class="cov5" title="5">defer resp.Body.Close()
- if err := handleOpenAINon2xx(resp, start); err != nil </span><span class="cov1" title="1">{
+ <span class="cov7" title="7">defer resp.Body.Close()
+ if err := handleOpenAINon2xx(resp, start); err != nil </span><span class="cov3" title="2">{
return "", err
}</span>
- <span class="cov5" title="4">out, err := decodeOpenAIChat(resp, start)
+ <span class="cov6" title="5">out, err := decodeOpenAIChat(resp, start)
if err != nil </span><span class="cov1" title="1">{
return "", err
}</span>
- <span class="cov4" title="3">if len(out.Choices) == 0 </span><span class="cov1" title="1">{
+ <span class="cov5" title="4">if len(out.Choices) == 0 </span><span class="cov1" title="1">{
logging.Logf("llm/openai ", "%sno choices returned duration=%s%s", logging.AnsiRed, time.Since(start), logging.AnsiBase)
return "", errors.New("openai: no choices returned")
}</span>
- <span class="cov3" title="2">content := out.Choices[0].Message.Content
+ <span class="cov4" title="3">content := out.Choices[0].Message.Content
logging.Logf("llm/openai ", "success choice=0 finish=%s size=%d preview=%s%s%s duration=%s", out.Choices[0].FinishReason, len(content), logging.AnsiGreen, logging.PreviewForLog(content), logging.AnsiBase, time.Since(start))
return content, nil</span>
}
// Provider metadata
-func (c openAIClient) Name() string <span class="cov6" title="6">{ return "openai" }</span>
-func (c openAIClient) DefaultModel() string <span class="cov6" title="6">{ return c.defaultModel }</span>
+func (c openAIClient) Name() string <span class="cov3" title="2">{ return "openai" }</span>
+func (c openAIClient) DefaultModel() string <span class="cov3" title="2">{ return c.defaultModel }</span>
// Streaming support (optional)
-func (c openAIClient) ChatStream(ctx context.Context, messages []Message, onDelta func(string), opts ...RequestOption) error <span class="cov5" title="4">{
+func (c openAIClient) ChatStream(ctx context.Context, messages []Message, onDelta func(string), opts ...RequestOption) error <span class="cov6" title="5">{
if c.apiKey == "" </span><span class="cov0" title="0">{
return errors.New("missing OpenAI API key")
}</span>
- <span class="cov5" title="4">o := Options{Model: c.defaultModel}
+ <span class="cov6" title="5">o := Options{Model: c.defaultModel}
for _, opt := range opts </span><span class="cov0" title="0">{
opt(&amp;o)
}</span>
- <span class="cov5" title="4">if o.Model == "" </span><span class="cov0" title="0">{
+ <span class="cov6" title="5">if o.Model == "" </span><span class="cov0" title="0">{
o.Model = c.defaultModel
}</span>
- <span class="cov5" title="4">start := time.Now()
+ <span class="cov6" title="5">start := time.Now()
c.logStart(true, o, messages)
req := buildOAChatRequest(o, messages, c.defaultTemperature, true)
body, err := json.Marshal(req)
@@ -3339,7 +3375,7 @@ func (c openAIClient) ChatStream(ctx context.Context, messages []Message, onDelt
c.logf("marshal error: %v", err)
return err
}</span>
- <span class="cov5" title="4">endpoint := c.baseURL + "/chat/completions"
+ <span class="cov6" title="5">endpoint := c.baseURL + "/chat/completions"
logging.Logf("llm/openai ", "POST %s (stream)", endpoint)
resp, err := c.doJSONWithAccept(ctx, endpoint, body, map[string]string{
"Authorization": "Bearer " + c.apiKey,
@@ -3348,15 +3384,15 @@ func (c openAIClient) ChatStream(ctx context.Context, messages []Message, onDelt
logging.Logf("llm/openai ", "%shttp error after %s: %v%s", logging.AnsiRed, time.Since(start), err, logging.AnsiBase)
return err
}</span>
- <span class="cov5" title="4">defer resp.Body.Close()
+ <span class="cov6" title="5">defer resp.Body.Close()
if err := handleOpenAINon2xx(resp, start); err != nil </span><span class="cov0" title="0">{
return err
}</span>
- <span class="cov5" title="4">if err := parseOpenAIStream(resp, start, onDelta); err != nil </span><span class="cov1" title="1">{
+ <span class="cov6" title="5">if err := parseOpenAIStream(resp, start, onDelta); err != nil </span><span class="cov1" title="1">{
return err
}</span>
- <span class="cov4" title="3">logging.Logf("llm/openai ", "stream end duration=%s", time.Since(start))
+ <span class="cov5" title="4">logging.Logf("llm/openai ", "stream end duration=%s", time.Since(start))
return nil</span>
}
@@ -3364,104 +3400,104 @@ func (c openAIClient) ChatStream(ctx context.Context, messages []Message, onDelt
func (c openAIClient) logf(format string, args ...any) <span class="cov0" title="0">{ logging.Logf("llm/openai ", format, args...) }</span>
// helpers extracted to keep methods small
-func (c openAIClient) logStart(stream bool, o Options, messages []Message) <span class="cov7" title="9">{
+func (c openAIClient) logStart(stream bool, o Options, messages []Message) <span class="cov8" title="12">{
logMessages := make([]struct{ Role, Content string }, len(messages))
- for i, m := range messages </span><span class="cov7" title="9">{
+ for i, m := range messages </span><span class="cov8" title="12">{
logMessages[i] = struct{ Role, Content string }{m.Role, m.Content}
}</span>
- <span class="cov7" title="9">c.chatLogger.LogStart(stream, o.Model, o.Temperature, o.MaxTokens, o.Stop, logMessages)</span>
+ <span class="cov8" title="12">c.chatLogger.LogStart(stream, o.Model, o.Temperature, o.MaxTokens, o.Stop, logMessages)</span>
}
-func buildOAChatRequest(o Options, messages []Message, defaultTemp *float64, stream bool) oaChatRequest <span class="cov8" title="14">{
+func buildOAChatRequest(o Options, messages []Message, defaultTemp *float64, stream bool) oaChatRequest <span class="cov9" title="15">{
req := oaChatRequest{Model: o.Model, Stream: stream}
req.Messages = make([]oaMessage, len(messages))
- for i, m := range messages </span><span class="cov8" title="14">{
+ for i, m := range messages </span><span class="cov9" title="15">{
req.Messages[i] = oaMessage{Role: m.Role, Content: m.Content}
}</span>
- <span class="cov8" title="14">if o.Temperature != 0 </span><span class="cov1" title="1">{
+ <span class="cov9" title="15">if o.Temperature != 0 </span><span class="cov1" title="1">{
req.Temperature = &amp;o.Temperature
- }</span> else<span class="cov8" title="13"> if defaultTemp != nil </span><span class="cov8" title="11">{
+ }</span> else<span class="cov9" title="14"> if defaultTemp != nil </span><span class="cov7" title="9">{
t := *defaultTemp
req.Temperature = &amp;t
}</span>
- <span class="cov8" title="14">if o.MaxTokens &gt; 0 </span><span class="cov5" title="5">{
+ <span class="cov9" title="15">if o.MaxTokens &gt; 0 </span><span class="cov4" title="3">{
if requiresMaxCompletionTokens(o.Model) </span><span class="cov3" title="2">{
req.MaxCompletionTokens = &amp;o.MaxTokens
- }</span> else<span class="cov4" title="3"> {
+ }</span> else<span class="cov1" title="1"> {
req.MaxTokens = &amp;o.MaxTokens
}</span>
}
- <span class="cov8" title="14">if len(o.Stop) &gt; 0 </span><span class="cov3" title="2">{
+ <span class="cov9" title="15">if len(o.Stop) &gt; 0 </span><span class="cov0" title="0">{
req.Stop = o.Stop
}</span>
// Enforce gpt-5 temperature constraints: only default (1.0) is supported.
- <span class="cov8" title="14">if requiresMaxCompletionTokens(o.Model) </span><span class="cov3" title="2">{
+ <span class="cov9" title="15">if requiresMaxCompletionTokens(o.Model) </span><span class="cov3" title="2">{
if req.Temperature == nil || *req.Temperature != 1.0 </span><span class="cov3" title="2">{
t := 1.0
req.Temperature = &amp;t
logging.Logf("llm/openai ", "forcing temperature=1.0 for model=%s (gpt-5 constraint)", o.Model)
}</span>
}
- <span class="cov8" title="14">return req</span>
+ <span class="cov9" title="15">return req</span>
}
// requiresMaxCompletionTokens reports whether the given model prefers the
// new parameter name "max_completion_tokens" instead of "max_tokens". Newer
// models (e.g., gpt-5 family) expect this per OpenAI's API error guidance.
-func requiresMaxCompletionTokens(model string) bool <span class="cov9" title="19">{
+func requiresMaxCompletionTokens(model string) bool <span class="cov10" title="18">{
m := strings.ToLower(strings.TrimSpace(model))
return strings.HasPrefix(m, "gpt-5")
}</span>
-func (c openAIClient) doJSON(ctx context.Context, url string, body []byte, headers map[string]string) (*http.Response, error) <span class="cov5" title="5">{
+func (c openAIClient) doJSON(ctx context.Context, url string, body []byte, headers map[string]string) (*http.Response, error) <span class="cov7" title="7">{
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil </span><span class="cov0" title="0">{
return nil, err
}</span>
- <span class="cov5" title="5">req.Header.Set("Content-Type", "application/json")
- for k, v := range headers </span><span class="cov5" title="5">{
+ <span class="cov7" title="7">req.Header.Set("Content-Type", "application/json")
+ for k, v := range headers </span><span class="cov7" title="7">{
req.Header.Set(k, v)
}</span>
- <span class="cov5" title="5">return c.httpClient.Do(req)</span>
+ <span class="cov7" title="7">return c.httpClient.Do(req)</span>
}
-func (c openAIClient) doJSONWithAccept(ctx context.Context, url string, body []byte, headers map[string]string, accept string) (*http.Response, error) <span class="cov5" title="4">{
+func (c openAIClient) doJSONWithAccept(ctx context.Context, url string, body []byte, headers map[string]string, accept string) (*http.Response, error) <span class="cov6" title="5">{
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil </span><span class="cov0" title="0">{
return nil, err
}</span>
- <span class="cov5" title="4">req.Header.Set("Content-Type", "application/json")
+ <span class="cov6" title="5">req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", accept)
- for k, v := range headers </span><span class="cov5" title="4">{
+ for k, v := range headers </span><span class="cov6" title="5">{
req.Header.Set(k, v)
}</span>
- <span class="cov5" title="4">return c.httpClient.Do(req)</span>
+ <span class="cov6" title="5">return c.httpClient.Do(req)</span>
}
-func handleOpenAINon2xx(resp *http.Response, start time.Time) error <span class="cov8" title="11">{
- if resp.StatusCode &gt;= 200 &amp;&amp; resp.StatusCode &lt; 300 </span><span class="cov7" title="8">{
+func handleOpenAINon2xx(resp *http.Response, start time.Time) error <span class="cov8" title="13">{
+ if resp.StatusCode &gt;= 200 &amp;&amp; resp.StatusCode &lt; 300 </span><span class="cov8" title="10">{
return nil
}</span>
<span class="cov4" title="3">var apiErr oaChatResponse
_ = json.NewDecoder(resp.Body).Decode(&amp;apiErr)
- if apiErr.Error != nil &amp;&amp; apiErr.Error.Message != "" </span><span class="cov3" title="2">{
+ if apiErr.Error != nil &amp;&amp; apiErr.Error.Message != "" </span><span class="cov1" title="1">{
logging.Logf("llm/openai ", "%sapi error status=%d type=%s msg=%s duration=%s%s", logging.AnsiRed, resp.StatusCode, apiErr.Error.Type, apiErr.Error.Message, time.Since(start), logging.AnsiBase)
return fmt.Errorf("openai error: %s (status %d)", apiErr.Error.Message, resp.StatusCode)
}</span>
- <span class="cov1" title="1">logging.Logf("llm/openai ", "%shttp non-2xx status=%d duration=%s%s", logging.AnsiRed, resp.StatusCode, time.Since(start), logging.AnsiBase)
+ <span class="cov3" title="2">logging.Logf("llm/openai ", "%shttp non-2xx status=%d duration=%s%s", logging.AnsiRed, resp.StatusCode, time.Since(start), logging.AnsiBase)
return fmt.Errorf("openai http error: status %d", resp.StatusCode)</span>
}
-func decodeOpenAIChat(resp *http.Response, start time.Time) (oaChatResponse, error) <span class="cov5" title="4">{
+func decodeOpenAIChat(resp *http.Response, start time.Time) (oaChatResponse, error) <span class="cov6" title="5">{
var out oaChatResponse
if err := json.NewDecoder(resp.Body).Decode(&amp;out); err != nil </span><span class="cov1" title="1">{
logging.Logf("llm/openai ", "%sdecode error after %s: %v%s", logging.AnsiRed, time.Since(start), err, logging.AnsiBase)
return oaChatResponse{}, err
}</span>
- <span class="cov4" title="3">return out, nil</span>
+ <span class="cov5" title="4">return out, nil</span>
}
-func parseOpenAIStream(resp *http.Response, start time.Time, onDelta func(string)) error <span class="cov5" title="5">{
+func parseOpenAIStream(resp *http.Response, start time.Time, onDelta func(string)) error <span class="cov6" title="5">{
// Parse SSE: lines starting with "data: " containing JSON or [DONE]
scanner := bufio.NewScanner(resp.Body)
const maxBuf = 1024 * 1024
@@ -3469,22 +3505,22 @@ func parseOpenAIStream(resp *http.Response, start time.Time, onDelta func(string
scanner.Buffer(buf, maxBuf)
for scanner.Scan() </span><span class="cov8" title="11">{
line := scanner.Text()
- if !strings.HasPrefix(line, "data: ") </span><span class="cov4" title="3">{
+ if !strings.HasPrefix(line, "data: ") </span><span class="cov3" title="2">{
continue</span>
}
- <span class="cov7" title="8">payload := strings.TrimPrefix(line, "data: ")
+ <span class="cov7" title="9">payload := strings.TrimPrefix(line, "data: ")
if strings.TrimSpace(payload) == "[DONE]" </span><span class="cov4" title="3">{
break</span>
}
- <span class="cov5" title="5">var chunk oaStreamChunk
+ <span class="cov6" title="6">var chunk oaStreamChunk
if err := json.Unmarshal([]byte(payload), &amp;chunk); err != nil </span><span class="cov3" title="2">{
continue</span>
}
- <span class="cov4" title="3">if chunk.Error != nil &amp;&amp; chunk.Error.Message != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="4">if chunk.Error != nil &amp;&amp; chunk.Error.Message != "" </span><span class="cov1" title="1">{
logging.Logf("llm/openai ", "%sstream error: %s%s", logging.AnsiRed, chunk.Error.Message, logging.AnsiBase)
return fmt.Errorf("openai stream error: %s", chunk.Error.Message)
}</span>
- <span class="cov3" title="2">for _, ch := range chunk.Choices </span><span class="cov3" title="2">{
+ <span class="cov4" title="3">for _, ch := range chunk.Choices </span><span class="cov4" title="3">{
if ch.Delta.Content != "" </span><span class="cov3" title="2">{
onDelta(ch.Delta.Content)
}</span>
@@ -3556,8 +3592,8 @@ type Options struct {
type RequestOption func(*Options)
func WithModel(model string) RequestOption <span class="cov1" title="1">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Model = model }</span> }
-func WithTemperature(t float64) RequestOption <span class="cov6" title="12">{ return func(o *Options) </span><span class="cov2" title="2">{ o.Temperature = t }</span> }
-func WithMaxTokens(n int) RequestOption <span class="cov10" title="49">{ return func(o *Options) </span><span class="cov2" title="2">{ o.MaxTokens = n }</span> }
+func WithTemperature(t float64) RequestOption <span class="cov7" title="15">{ return func(o *Options) </span><span class="cov2" title="2">{ o.Temperature = t }</span> }
+func WithMaxTokens(n int) RequestOption <span class="cov10" title="53">{ return func(o *Options) </span><span class="cov2" title="2">{ o.MaxTokens = n }</span> }
func WithStop(stop ...string) RequestOption <span class="cov1" title="1">{
return func(o *Options) </span><span class="cov1" title="1">{ o.Stop = append([]string{}, stop...) }</span>
}
@@ -3582,14 +3618,14 @@ type Config struct {
// NewFromConfig creates an LLM client using only the supplied configuration.
// The OpenAI API key is supplied separately and may be read from the environment
// by the caller; other environment-based configuration is not used.
-func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, error) <span class="cov8" title="22">{
+func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, error) <span class="cov8" title="23">{
p := strings.ToLower(strings.TrimSpace(cfg.Provider))
- if p == "" </span><span class="cov5" title="8">{
+ if p == "" </span><span class="cov5" title="9">{
p = "openai"
}</span>
- <span class="cov8" title="22">switch p </span>{
- case "openai":<span class="cov7" title="15">
- if strings.TrimSpace(openAIAPIKey) == "" </span><span class="cov4" title="5">{
+ <span class="cov8" title="23">switch p </span>{
+ case "openai":<span class="cov7" title="16">
+ if strings.TrimSpace(openAIAPIKey) == "" </span><span class="cov6" title="10">{
return nil, errors.New("missing OPENAI_API_KEY for provider openai")
}</span>
// Default temperature selection:
@@ -3598,7 +3634,7 @@ func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, erro
// The app-wide defaults currently set provider temps to 0.2.
// If the user hasn't explicitly overridden and the model is gpt-5*,
// upgrade 0.2 → 1.0 to satisfy the requested default for gpt-5.
- <span class="cov6" title="10">model := strings.ToLower(strings.TrimSpace(cfg.OpenAIModel))
+ <span class="cov5" title="6">model := strings.ToLower(strings.TrimSpace(cfg.OpenAIModel))
if strings.HasPrefix(model, "gpt-5") </span><span class="cov2" title="2">{
if cfg.OpenAITemperature == nil </span><span class="cov1" title="1">{
v := 1.0
@@ -3607,11 +3643,11 @@ func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, erro
v := 1.0
cfg.OpenAITemperature = &amp;v
}</span>
- } else<span class="cov5" title="8"> if cfg.OpenAITemperature == nil </span><span class="cov5" title="6">{
+ } else<span class="cov4" title="4"> if cfg.OpenAITemperature == nil </span><span class="cov3" title="3">{
v := 0.2
cfg.OpenAITemperature = &amp;v
}</span>
- <span class="cov6" title="10">return newOpenAI(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature), nil</span>
+ <span class="cov5" title="6">return newOpenAI(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature), nil</span>
case "ollama":<span class="cov3" title="3">
if cfg.OllamaTemperature == nil </span><span class="cov2" title="2">{
t := 0.2
@@ -3685,7 +3721,7 @@ type ChatLogger struct {
}
// NewChatLogger creates a new ChatLogger for a given provider.
-func NewChatLogger(provider string) ChatLogger <span class="cov10" title="43">{
+func NewChatLogger(provider string) ChatLogger <span class="cov10" title="42">{
return ChatLogger{Provider: provider}
}</span>
@@ -3694,14 +3730,14 @@ func (cl ChatLogger) LogStart(stream bool, model string, temp float64, maxTokens
Role string
Content string
},
-) <span class="cov8" title="24">{
+) <span class="cov8" title="27">{
chatOrStream := "chat"
- if stream </span><span class="cov5" title="8">{
+ if stream </span><span class="cov6" title="9">{
chatOrStream = "stream"
}</span>
- <span class="cov8" title="24">Logf("llm/"+cl.Provider+" ", "%s start model=%s temp=%.2f max_tokens=%d stop=%d messages=%d",
+ <span class="cov8" title="27">Logf("llm/"+cl.Provider+" ", "%s start model=%s temp=%.2f max_tokens=%d stop=%d messages=%d",
chatOrStream, model, temp, maxTokens, len(stop), len(messages))
- for i, m := range messages </span><span class="cov8" title="24">{
+ for i, m := range messages </span><span class="cov8" title="27">{
Logf("llm/"+cl.Provider+" ", "msg[%d] role=%s size=%d preview=%s%s%s",
i, m.Role, len(m.Content), AnsiCyan, PreviewForLog(m.Content), AnsiBase)
}</span>
@@ -3737,11 +3773,11 @@ var std *log.Logger
func Bind(l *log.Logger) <span class="cov2" title="3">{ std = l }</span>
// Logf prints a formatted message with a module prefix and base ANSI style.
-func Logf(prefix, format string, args ...any) <span class="cov10" title="183">{
- if std == nil </span><span class="cov9" title="129">{
+func Logf(prefix, format string, args ...any) <span class="cov10" title="199">{
+ if std == nil </span><span class="cov9" title="141">{
return
}</span>
- <span class="cov7" title="54">msg := fmt.Sprintf(format, args...)
+ <span class="cov7" title="58">msg := fmt.Sprintf(format, args...)
std.Print(AnsiBase + prefix + msg + AnsiReset)</span>
}
@@ -3750,21 +3786,86 @@ var logPreviewLimit int // 0 means unlimited
// SetLogPreviewLimit sets the maximum number of characters to log for
// request/response previews. Set to 0 for unlimited.
-func SetLogPreviewLimit(n int) <span class="cov4" title="9">{ logPreviewLimit = n }</span>
+func SetLogPreviewLimit(n int) <span class="cov5" title="11">{ logPreviewLimit = n }</span>
// PreviewForLog returns the string truncated to the configured preview limit.
-func PreviewForLog(s string) string <span class="cov6" title="32">{
+func PreviewForLog(s string) string <span class="cov7" title="36">{
if logPreviewLimit &gt; 0 </span><span class="cov2" title="3">{
if len(s) &lt;= logPreviewLimit </span><span class="cov0" title="0">{
return s
}</span>
<span class="cov2" title="3">return s[:logPreviewLimit] + "…"</span>
}
- <span class="cov6" title="29">return s</span>
+ <span class="cov6" title="33">return s</span>
+}
+</pre>
+
+ <pre class="file" id="file22" style="display: none">package lsp
+
+import (
+ "fmt"
+ "strings"
+
+ "codeberg.org/snonux/hexai/internal/appconfig"
+ "codeberg.org/snonux/hexai/internal/runtimeconfig"
+)
+
+type chatCommandResult struct {
+ message string
+}
+
+func (s *Server) chatCommandResponse(uri string, lineIdx int, prompt string) (chatCommandResult, bool) <span class="cov10" title="8">{
+ trimmed := strings.TrimSpace(s.stripTrailingTrigger(prompt))
+ if trimmed == "" || !strings.HasPrefix(trimmed, "/") </span><span class="cov10" title="8">{
+ return chatCommandResult{}, false
+ }</span>
+
+ <span class="cov0" title="0">switch </span>{
+ case strings.HasPrefix(trimmed, "/reload"):<span class="cov0" title="0">
+ return s.handleReloadCommand(), true</span>
+ case strings.HasPrefix(trimmed, "/help"):<span class="cov0" title="0">
+ return s.handleHelpCommand(), true</span>
+ default:<span class="cov0" title="0">
+ return chatCommandResult{message: fmt.Sprintf("Unknown command %q. Try /help?&gt;", trimmed)}, true</span>
+ }
+}
+
+func (s *Server) handleHelpCommand() chatCommandResult <span class="cov1" title="1">{
+ lines := []string{
+ "Available slash commands:",
+ "- /reload?&gt; reload configuration from file (ignores env overrides)",
+ }
+ return chatCommandResult{message: strings.Join(lines, "\n")}
+}</span>
+
+func (s *Server) handleReloadCommand() chatCommandResult <span class="cov1" title="1">{
+ 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})
+ 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)
+ 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">{
+ return "Reloaded config (no changes detected)."
+ }</span>
+ <span class="cov4" 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>
}
</pre>
- <pre class="file" id="file22" style="display: none">// Summary: Builds additional context snippets based on configured mode and truncates text by token heuristic.
+ <pre class="file" id="file23" style="display: none">// Summary: Builds additional context snippets based on configured mode and truncates text by token heuristic.
package lsp
import (
@@ -3780,20 +3881,20 @@ import (
// - file-on-new-func: include full file only when defining a new function
// - always-full: always include the full file
func (s *Server) buildAdditionalContext(newFunc bool, uri string, pos Position) (string, bool) <span class="cov10" title="13">{
- mode := s.contextMode
+ mode := s.contextMode()
switch mode </span>{
case "minimal":<span class="cov3" title="2">
return "", false</span>
case "window":<span class="cov1" title="1">
return s.windowContext(uri, pos), true</span>
- case "file-on-new-func":<span class="cov7" title="6">
+ case "file-on-new-func":<span class="cov8" title="8">
if newFunc </span><span class="cov3" title="2">{
return s.fullFileContext(uri), true
}</span>
- <span class="cov5" title="4">return "", false</span>
+ <span class="cov7" title="6">return "", false</span>
case "always-full":<span class="cov3" title="2">
return s.fullFileContext(uri), true</span>
- default:<span class="cov3" title="2">
+ default:<span class="cov0" title="0">
// fallback to minimal if unknown
return "", false</span>
}
@@ -3806,7 +3907,7 @@ func (s *Server) windowContext(uri string, pos Position) string <span class="cov
return ""
}</span>
<span class="cov3" title="2">n := len(d.lines)
- half := s.windowLines / 2
+ half := s.windowLines() / 2
start := pos.Line - half
if start &lt; 0 </span><span class="cov0" title="0">{
start = 0
@@ -3816,7 +3917,7 @@ func (s *Server) windowContext(uri string, pos Position) string <span class="cov
end = n
}</span>
<span class="cov3" title="2">text := strings.Join(d.lines[start:end], "\n")
- return truncateToApproxTokens(text, s.maxContextTokens)</span>
+ return truncateToApproxTokens(text, s.maxContextTokens())</span>
}
func (s *Server) fullFileContext(uri string) string <span class="cov5" title="4">{
@@ -3825,7 +3926,7 @@ func (s *Server) fullFileContext(uri string) string <span class="cov5" title="4"
logging.Logf("lsp ", "context: full-file requested but document not open; skipping uri=%s", uri)
return ""
}</span>
- <span class="cov5" title="4">return truncateToApproxTokens(d.text, s.maxContextTokens)</span>
+ <span class="cov5" title="4">return truncateToApproxTokens(d.text, s.maxContextTokens())</span>
}
// truncateToApproxTokens naively truncates the input to fit approx N tokens.
@@ -3850,7 +3951,7 @@ func truncateToApproxTokens(text string, maxTokens int) string <span class="cov7
}
</pre>
- <pre class="file" id="file23" style="display: none">// Summary: In-memory document model for the LSP; tracks text, lines, and applies edits.
+ <pre class="file" id="file24" style="display: none">// Summary: In-memory document model for the LSP; tracks text, lines, and applies edits.
package lsp
import (
@@ -3882,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="82">{
+func (s *Server) getDocument(uri string) *document <span class="cov10" title="85">{
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="cov9" title="51">{
+func splitLines(sx string) []string <span class="cov8" title="51">{
sx = strings.ReplaceAll(sx, "\r\n", "\n")
return strings.Split(sx, "\n")
}</span>
@@ -3988,16 +4089,16 @@ func trimLen(s string) string <span class="cov8" title="42">{
<span class="cov8" title="41">return s</span>
}
-func firstLine(s string) string <span class="cov7" title="25">{
+func firstLine(s string) string <span class="cov7" title="26">{
s = strings.ReplaceAll(s, "\r\n", "\n")
if idx := strings.IndexByte(s, '\n'); idx &gt;= 0 </span><span class="cov4" title="6">{
return s[:idx]
}</span>
- <span class="cov7" title="19">return s</span>
+ <span class="cov7" title="20">return s</span>
}
</pre>
- <pre class="file" id="file24" style="display: none">// Summary: LSP JSON-RPC handlers; implements core methods and integrates with the LLM client when enabled.
+ <pre class="file" id="file25" style="display: none">// Summary: LSP JSON-RPC handlers; implements core methods and integrates with the LLM client when enabled.
package lsp
import (
@@ -4050,7 +4151,8 @@ func (s *Server) findFirstInstructionInLine(line string) (instr string, cleaned
text string
}
cands := []cand{}
- if t, l, r, ok := findStrictInlineTag(line, s.inlineOpenChar, s.inlineCloseChar); ok </span><span class="cov5" title="6">{
+ _, _, openChar, closeChar := s.inlineMarkers()
+ if t, l, r, ok := findStrictInlineTag(line, openChar, closeChar); ok </span><span class="cov5" title="6">{
cands = append(cands, cand{start: l, end: r, text: t})
}</span>
<span class="cov9" title="24">if i := strings.Index(line, "/*"); i &gt;= 0 </span><span class="cov2" title="2">{
@@ -4187,33 +4289,33 @@ func (s *Server) reply(id json.RawMessage, result any, err *RespError) <span cla
// --- small completion cache (last ~10 entries) ---
-func (s *Server) completionCacheKey(p CompletionParams, above, current, below, funcCtx string, inParams bool, hasExtra bool, extraText string) string <span class="cov7" title="13">{
+func (s *Server) completionCacheKey(p CompletionParams, above, current, below, funcCtx string, inParams bool, hasExtra bool, extraText string) string <span class="cov8" title="14">{
// Normalize left-of-cursor by trimming trailing spaces/tabs
idx := p.Position.Character
if idx &gt; len(current) </span><span class="cov0" title="0">{
idx = len(current)
}</span>
- <span class="cov7" title="13">left := strings.TrimRight(current[:idx], " \t")
+ <span class="cov8" title="14">left := strings.TrimRight(current[:idx], " \t")
right := ""
if idx &lt; len(current) </span><span class="cov1" title="1">{
right = current[idx:]
}</span>
- <span class="cov7" title="13">prov := ""
+ <span class="cov8" title="14">prov := ""
model := ""
- if s.llmClient != nil </span><span class="cov7" title="13">{
- prov = s.llmClient.Name()
- model = s.llmClient.DefaultModel()
+ if client := s.currentLLMClient(); client != nil </span><span class="cov8" title="14">{
+ prov = client.Name()
+ model = client.DefaultModel()
}</span>
- <span class="cov7" title="13">temp := ""
- if s.codingTemperature != nil </span><span class="cov0" title="0">{
- temp = fmt.Sprintf("%.3f", *s.codingTemperature)
+ <span class="cov8" title="14">temp := ""
+ if tempPtr := s.codingTemperature(); tempPtr != nil </span><span class="cov0" title="0">{
+ temp = fmt.Sprintf("%.3f", *tempPtr)
}</span>
- <span class="cov7" title="13">extra := ""
+ <span class="cov8" title="14">extra := ""
if hasExtra </span><span class="cov0" title="0">{
extra = strings.TrimSpace(extraText)
}</span>
// Compose a key from essential context parts
- <span class="cov7" title="13">return strings.Join([]string{
+ <span class="cov8" title="14">return strings.Join([]string{
"v1", // version for future-proofing
prov,
model,
@@ -4230,11 +4332,11 @@ func (s *Server) completionCacheKey(p CompletionParams, above, current, below, f
}, "\x1f")</span> // use unit separator to avoid collisions
}
-func (s *Server) completionCacheGet(key string) (string, bool) <span class="cov6" title="9">{
+func (s *Server) completionCacheGet(key string) (string, bool) <span class="cov7" title="10">{
s.mu.Lock()
defer s.mu.Unlock()
v, ok := s.compCache[key]
- if !ok </span><span class="cov6" title="8">{
+ if !ok </span><span class="cov6" title="9">{
return "", false
}</span>
// move to most-recent
@@ -4242,13 +4344,13 @@ func (s *Server) completionCacheGet(key string) (string, bool) <span class="cov6
return v, true</span>
}
-func (s *Server) completionCachePut(key, value string) <span class="cov7" title="11">{
+func (s *Server) completionCachePut(key, value string) <span class="cov7" title="12">{
s.mu.Lock()
defer s.mu.Unlock()
- if s.compCache == nil </span><span class="cov3" title="3">{
+ if s.compCache == nil </span><span class="cov5" title="5">{
s.compCache = make(map[string]string)
}</span>
- <span class="cov7" title="11">if _, exists := s.compCache[key]; !exists </span><span class="cov7" title="11">{
+ <span class="cov7" title="12">if _, exists := s.compCache[key]; !exists </span><span class="cov7" title="12">{
s.compCacheOrder = append(s.compCacheOrder, key)
s.compCache[key] = value
if len(s.compCacheOrder) &gt; 10 </span><span class="cov0" title="0">{
@@ -4257,7 +4359,7 @@ func (s *Server) completionCachePut(key, value string) <span class="cov7" title=
s.compCacheOrder = s.compCacheOrder[1:]
delete(s.compCache, old)
}</span>
- <span class="cov7" title="11">return</span>
+ <span class="cov7" title="12">return</span>
}
// update existing and mark most-recent
<span class="cov0" title="0">s.compCache[key] = value
@@ -4285,6 +4387,8 @@ func (s *Server) compCacheTouchLocked(key string) <span class="cov1" title="1">{
// CompletionContext if provided and also falls back to inspecting the character
// immediately to the left of the cursor.
func (s *Server) isTriggerEvent(p CompletionParams, current string) bool <span class="cov9" title="25">{
+ open, _, openChar, closeChar := s.inlineMarkers()
+ triggerChars := s.triggerCharacters()
// 1) Inspect LSP completion context if present
if p.Context != nil </span><span class="cov7" title="11">{
var ctx struct {
@@ -4299,7 +4403,7 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool <span c
}</span>
// If configured and the line contains a bare double-open marker (e.g., '&gt;&gt;' with no '&gt;&gt;text&gt;'),
// do not treat as a trigger source.
- <span class="cov7" title="11">if s.inlineOpen != "" &amp;&amp; strings.Contains(current, s.inlineOpen+s.inlineOpen) &amp;&amp; !hasDoubleOpenTrigger(current, s.inlineOpenChar, s.inlineCloseChar) </span><span class="cov2" title="2">{
+ <span class="cov7" title="11">if open != "" &amp;&amp; strings.Contains(current, open+open) &amp;&amp; !hasDoubleOpenTrigger(current, openChar, closeChar) </span><span class="cov2" title="2">{
return false
}</span>
// TriggerKind 1 = Invoked (manual). Always allow manual invoke.
@@ -4309,12 +4413,12 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool <span c
// TriggerKind 2 is TriggerCharacter per LSP spec
<span class="cov4" title="4">if ctx.TriggerKind == 2 </span><span class="cov3" title="3">{
if ctx.TriggerCharacter != "" </span><span class="cov2" title="2">{
- for _, c := range s.triggerChars </span><span class="cov1" title="1">{
- if c == ctx.TriggerCharacter </span><span class="cov1" title="1">{
+ for _, c := range triggerChars </span><span class="cov2" title="2">{
+ if c == ctx.TriggerCharacter </span><span class="cov2" title="2">{
return true
}</span>
}
- <span class="cov1" title="1">return false</span>
+ <span class="cov0" title="0">return false</span>
}
// No character provided but reported as TriggerCharacter; be conservative
<span class="cov1" title="1">return false</span>
@@ -4327,11 +4431,11 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool <span c
return false
}</span>
// Bare double-open should not trigger via fallback char either (only when configured)
- <span class="cov8" title="15">if s.inlineOpen != "" &amp;&amp; strings.Contains(current, s.inlineOpen+s.inlineOpen) &amp;&amp; !hasDoubleOpenTrigger(current, s.inlineOpenChar, s.inlineCloseChar) </span><span class="cov3" title="3">{
+ <span class="cov8" title="15">if open != "" &amp;&amp; strings.Contains(current, open+open) &amp;&amp; !hasDoubleOpenTrigger(current, openChar, closeChar) </span><span class="cov3" title="3">{
return false
}</span>
<span class="cov7" title="12">ch := string(current[idx-1])
- for _, c := range s.triggerChars </span><span class="cov9" title="28">{
+ for _, c := range triggerChars </span><span class="cov9" title="28">{
if c == ch </span><span class="cov5" title="6">{
return true
}</span>
@@ -4339,15 +4443,15 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool <span c
<span class="cov5" title="6">return false</span>
}
-func (s *Server) makeCompletionItems(cleaned string, inParams bool, current string, p CompletionParams, docStr string) []CompletionItem <span class="cov7" title="12">{
+func (s *Server) makeCompletionItems(cleaned string, inParams bool, current string, p CompletionParams, docStr string) []CompletionItem <span class="cov7" title="13">{
te, filter := computeTextEditAndFilter(cleaned, inParams, current, p)
rm := s.collectPromptRemovalEdits(p.TextDocument.URI)
label := labelForCompletion(cleaned, filter)
detail := "Hexai LLM completion"
- if s.llmClient != nil </span><span class="cov7" title="12">{
- detail = "Hexai " + s.llmClient.Name() + ":" + s.llmClient.DefaultModel()
+ if client := s.currentLLMClient(); client != nil </span><span class="cov7" title="13">{
+ detail = "Hexai " + client.Name() + ":" + client.DefaultModel()
}</span>
- <span class="cov7" title="12">return []CompletionItem{{
+ <span class="cov7" title="13">return []CompletionItem{{
Label: label,
Kind: 1,
Detail: detail,
@@ -4444,7 +4548,7 @@ func (s *Server) fallbackCompletionItems(docStr string) []CompletionItem <span c
}</span>
</pre>
- <pre class="file" id="file25" style="display: none">// Summary: Code Action handlers and helpers split from handlers.go for clarity.
+ <pre class="file" id="file26" style="display: none">// Summary: Code Action handlers and helpers split from handlers.go for clarity.
package lsp
import (
@@ -4469,7 +4573,7 @@ func (s *Server) handleCodeAction(req Request) <span class="cov4" title="5">{
<span class="cov0" title="0">return</span>
}
<span class="cov4" title="5">d := s.getDocument(p.TextDocument.URI)
- if d == nil || len(d.lines) == 0 || s.llmClient == nil </span><span class="cov2" title="2">{
+ if d == nil || len(d.lines) == 0 || s.currentLLMClient() == nil </span><span class="cov2" title="2">{
if len(req.ID) != 0 </span><span class="cov2" title="2">{
s.reply(req.ID, []CodeAction{}, nil)
}</span>
@@ -4502,11 +4606,12 @@ func (s *Server) handleCodeAction(req Request) <span class="cov4" title="5">{
// appendCustomActions adds user-defined actions depending on scope and availability.
func (s *Server) appendCustomActions(actions *[]CodeAction, p CodeActionParams, sel string) <span class="cov3" title="3">{
- if len(s.customActions) == 0 </span><span class="cov1" title="1">{
+ customs := s.customActions()
+ if len(customs) == 0 </span><span class="cov1" title="1">{
return
}</span>
<span class="cov2" title="2">diags := s.diagnosticsInRange(p.Context, p.Range)
- for _, ca := range s.customActions </span><span class="cov3" title="4">{
+ for _, ca := range customs </span><span class="cov3" title="4">{
title := strings.TrimSpace(ca.Title)
if title == "" </span><span class="cov0" title="0">{
continue</span>
@@ -4601,7 +4706,7 @@ func (s *Server) buildDiagnosticsCodeAction(p CodeActionParams, sel string) *Cod
}
func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class="cov6" title="17">{
- if s.llmClient == nil || len(ca.Data) == 0 </span><span class="cov1" title="1">{
+ if s.currentLLMClient() == nil || len(ca.Data) == 0 </span><span class="cov1" title="1">{
return ca, false
}</span>
<span class="cov6" title="16">var payload struct {
@@ -4616,25 +4721,14 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class
if err := json.Unmarshal(ca.Data, &amp;payload); err != nil </span><span class="cov0" title="0">{
return ca, false
}</span>
- <span class="cov6" title="16">switch payload.Type </span>{
+ <span class="cov6" title="16">cfg := s.currentConfig()
+ switch payload.Type </span>{
case "rewrite":<span class="cov3" title="4">
- sys := s.promptRewriteSystem
- user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": payload.Instruction, "selection": payload.Selection})
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
- defer cancel()
- messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
- opts := s.llmRequestOpts()
- if text, err := s.chatWithStats(ctx, messages, opts...); err == nil </span><span class="cov3" title="4">{
- if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov3" title="4">{
- edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}}
- ca.Edit = &amp;edit
- return ca, true
- }</span>
- } else<span class="cov0" title="0"> {
- logging.Logf("lsp ", "codeAction rewrite llm error: %v", err)
- }</span>
+ sys := cfg.PromptCodeActionRewriteSystem
+ user := renderTemplate(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": payload.Instruction, "selection": payload.Selection})
+ return s.completeCodeAction(ca, payload.URI, payload.Range, sys, user, 20*time.Second)</span>
case "diagnostics":<span class="cov4" title="5">
- sys := s.promptDiagnosticsSystem
+ sys := cfg.PromptCodeActionDiagnosticsSystem
var b strings.Builder
for i, dgn := range payload.Diagnostics </span><span class="cov4" title="6">{
if dgn.Source != "" </span><span class="cov0" title="0">{
@@ -4644,115 +4738,73 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class
}</span>
}
<span class="cov4" title="5">diagList := b.String()
- user := renderTemplate(s.promptDiagnosticsUser, map[string]string{"diagnostics": diagList, "selection": payload.Selection})
- ctx, cancel := context.WithTimeout(context.Background(), 22*time.Second)
- defer cancel()
- messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
- opts := s.llmRequestOpts()
- if text, err := s.chatWithStats(ctx, messages, opts...); err == nil </span><span class="cov4" title="5">{
- if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov4" title="5">{
- edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}}
- ca.Edit = &amp;edit
- return ca, true
- }</span>
- } else<span class="cov0" title="0"> {
- logging.Logf("lsp ", "codeAction diagnostics llm error: %v", err)
- }</span>
+ user := renderTemplate(cfg.PromptCodeActionDiagnosticsUser, map[string]string{"diagnostics": diagList, "selection": payload.Selection})
+ return s.completeCodeAction(ca, payload.URI, payload.Range, sys, user, 22*time.Second)</span>
case "document":<span class="cov3" title="3">
- sys := s.promptDocumentSystem
- user := renderTemplate(s.promptDocumentUser, map[string]string{"selection": payload.Selection})
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
- defer cancel()
- messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
- opts := s.llmRequestOpts()
- if text, err := s.chatWithStats(ctx, messages, opts...); err == nil </span><span class="cov3" title="3">{
- if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov3" title="3">{
- edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}}
- ca.Edit = &amp;edit
- return ca, true
- }</span>
- } else<span class="cov0" title="0"> {
- logging.Logf("lsp ", "codeAction document llm error: %v", err)
- }</span>
+ sys := cfg.PromptCodeActionDocumentSystem
+ user := renderTemplate(cfg.PromptCodeActionDocumentUser, map[string]string{"selection": payload.Selection})
+ return s.completeCodeAction(ca, payload.URI, payload.Range, sys, user, 20*time.Second)</span>
case "go_test":<span class="cov0" title="0">
if edit, jumpURI, jumpRange, ok := s.resolveGoTest(payload.URI, payload.Range.Start); ok </span><span class="cov0" title="0">{
ca.Edit = &amp;edit
- // After edit is applied, ask client to jump to new test function
ca.Command = &amp;Command{Title: "Jump to generated test", Command: "hexai.showDocument", Arguments: []any{jumpURI, jumpRange}}
- // Also send a server-initiated showDocument shortly after resolve to cover
- // clients that do not execute commands from code actions.
s.deferShowDocument(jumpURI, jumpRange)
return ca, true
}</span>
case "simplify":<span class="cov0" title="0">
- sys := s.promptRewriteSystem
- // Reuse rewrite user template with a fixed instruction
- user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": "Simplify and improve the code while preserving behavior. Return only the improved code.", "selection": payload.Selection})
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
- defer cancel()
- messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
- opts := s.llmRequestOpts()
- if text, err := s.chatWithStats(ctx, messages, opts...); err == nil </span><span class="cov0" title="0">{
- if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov0" title="0">{
- edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}}
- ca.Edit = &amp;edit
- return ca, true
- }</span>
- } else<span class="cov0" title="0"> {
- logging.Logf("lsp ", "codeAction simplify llm error: %v", err)
- }</span>
+ sys := cfg.PromptCodeActionRewriteSystem
+ user := renderTemplate(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": "Simplify and improve the code while preserving behavior. Return only the improved code.", "selection": payload.Selection})
+ return s.completeCodeAction(ca, payload.URI, payload.Range, sys, user, 20*time.Second)</span>
case "custom":<span class="cov3" title="4">
- // Lookup action by ID
var action *CustomAction
- for i := range s.customActions </span><span class="cov4" title="5">{
- if s.customActions[i].ID == payload.ID </span><span class="cov3" title="4">{
- action = &amp;s.customActions[i]
+ for _, caDef := range s.customActions() </span><span class="cov4" title="5">{
+ if caDef.ID == payload.ID </span><span class="cov3" title="4">{
+ action = &amp;caDef
break</span>
}
}
<span class="cov3" title="4">if action == nil </span><span class="cov0" title="0">{
return ca, false
}</span>
- // Build messages
<span class="cov3" title="4">var sys, user string
if strings.TrimSpace(action.User) != "" </span><span class="cov1" title="1">{
if strings.TrimSpace(action.System) != "" </span><span class="cov0" title="0">{
sys = action.System
}</span> else<span class="cov1" title="1"> {
- sys = s.promptRewriteSystem
+ sys = cfg.PromptCodeActionRewriteSystem
}</span>
<span class="cov1" title="1">var diagList string
if len(payload.Diagnostics) &gt; 0 </span><span class="cov1" title="1">{
var b strings.Builder
- for i, dgn := range payload.Diagnostics </span><span class="cov1" title="1">{
- if dgn.Source != "" </span><span class="cov0" title="0">{
- fmt.Fprintf(&amp;b, "%d. [%s] %s\n", i+1, dgn.Source, dgn.Message)
- }</span> else<span class="cov1" title="1"> {
- fmt.Fprintf(&amp;b, "%d. %s\n", i+1, dgn.Message)
- }</span>
- }
+ for _, d := range payload.Diagnostics </span><span class="cov1" title="1">{
+ fmt.Fprintf(&amp;b, "%s\n", d.Message)
+ }</span>
<span class="cov1" title="1">diagList = b.String()</span>
}
- <span class="cov1" title="1">user = renderTemplate(action.User, map[string]string{"selection": payload.Selection, "diagnostics": diagList})</span>
+ <span class="cov1" title="1">user = renderTemplate(action.User, map[string]string{"selection": payload.Selection, "diagnostics": strings.TrimSpace(diagList)})</span>
} else<span class="cov3" title="3"> {
- // Use rewrite templates with fixed instruction
- sys = s.promptRewriteSystem
- user = renderTemplate(s.promptRewriteUser, map[string]string{"instruction": action.Instruction, "selection": payload.Selection})
- }</span>
- <span class="cov3" title="4">ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
- defer cancel()
- messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
- opts := s.llmRequestOpts()
- if text, err := s.chatWithStats(ctx, messages, opts...); err == nil </span><span class="cov3" title="3">{
- if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov2" title="2">{
- edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}}
- ca.Edit = &amp;edit
- return ca, true
- }</span>
- } else<span class="cov1" title="1"> {
- logging.Logf("lsp ", "codeAction custom id=%s llm error: %v", action.ID, err)
+ sys = cfg.PromptCodeActionRewriteSystem
+ user = renderTemplate(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": payload.Instruction, "selection": payload.Selection})
}</span>
+ <span class="cov3" title="4">return s.completeCodeAction(ca, payload.URI, payload.Range, sys, user, 20*time.Second)</span>
}
+ <span class="cov0" title="0">return ca, false</span>
+}
+
+func (s *Server) completeCodeAction(ca CodeAction, uri string, rng Range, sys, user string, timeout time.Duration) (CodeAction, bool) <span class="cov6" title="16">{
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+ messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
+ opts := s.llmRequestOpts()
+ if text, err := s.chatWithStats(ctx, messages, opts...); err == nil </span><span class="cov6" title="15">{
+ if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov6" title="14">{
+ edit := WorkspaceEdit{Changes: map[string][]TextEdit{uri: {{Range: rng, NewText: out}}}}
+ ca.Edit = &amp;edit
+ return ca, true
+ }</span>
+ } else<span class="cov1" title="1"> {
+ logging.Logf("lsp ", "codeAction llm error: %v", err)
+ }</span>
<span class="cov2" title="2">return ca, false</span>
}
@@ -4856,7 +4908,7 @@ func (s *Server) buildGoUnitTestCodeAction(p CodeActionParams) *CodeAction <span
// buildDocumentCodeAction offers to document the selected code by injecting comments.
func (s *Server) buildDocumentCodeAction(p CodeActionParams, sel string) *CodeAction <span class="cov3" title="4">{
- if s.llmClient == nil </span><span class="cov0" title="0">{
+ if s.currentLLMClient() == nil </span><span class="cov0" title="0">{
return nil
}</span>
<span class="cov3" title="4">if strings.TrimSpace(sel) == "" </span><span class="cov1" title="1">{
@@ -5053,9 +5105,10 @@ func findGoFunctionAtLine(lines []string, idx int) (int, int) <span class="cov3"
// generateGoTestFunction uses LLM to produce a test function; falls back to a stub when unavailable.
func (s *Server) generateGoTestFunction(funcCode string) string <span class="cov3" title="4">{
- if s.llmClient != nil </span><span class="cov2" title="2">{
- sys := s.promptGoTestSystem
- user := renderTemplate(s.promptGoTestUser, map[string]string{"function": funcCode})
+ if client := s.currentLLMClient(); client != nil </span><span class="cov2" title="2">{
+ cfg := s.currentConfig()
+ sys := cfg.PromptCodeActionGoTestSystem
+ user := renderTemplate(cfg.PromptCodeActionGoTestUser, map[string]string{"function": funcCode})
ctx, cancel := context.WithTimeout(context.Background(), 18*time.Second)
defer cancel()
messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
@@ -5112,7 +5165,7 @@ func exportName(name string) string <span class="cov2" title="2">{
}
</pre>
- <pre class="file" id="file26" style="display: none">// Summary: Completion handlers split from handlers.go to reduce file size and isolate feature logic.
+ <pre class="file" id="file27" style="display: none">// Summary: Completion handlers split from handlers.go to reduce file size and isolate feature logic.
package lsp
import (
@@ -5205,15 +5258,15 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun
defer cancel()
plan, items, handled := s.prepareCompletionPlan(p, above, current, below, funcCtx, docStr, hasExtra, extraText)
- if handled </span><span class="cov6" title="10">{
+ if handled </span><span class="cov6" title="9">{
return items, true
}</span>
- <span class="cov6" title="8">if items, ok := s.tryProviderNativeCompletion(current, p, above, below, funcCtx, docStr, hasExtra, extraText, plan.inParams); ok </span><span class="cov1" title="1">{
+ <span class="cov6" title="9">if items, ok := s.tryProviderNativeCompletion(current, p, above, below, funcCtx, docStr, hasExtra, extraText, plan.inParams); ok </span><span class="cov1" title="1">{
return items, true
}</span>
- <span class="cov5" title="7">return s.executeChatCompletion(ctx, plan)</span>
+ <span class="cov6" title="8">return s.executeChatCompletion(ctx, plan)</span>
}
func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below, funcCtx, docStr string, hasExtra bool, extraText string) (completionPlan, []CompletionItem, bool) <span class="cov8" title="18">{
@@ -5227,15 +5280,16 @@ func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below
hasExtra: hasExtra,
extraText: extraText,
}
- plan.inlinePrompt = lineHasInlinePrompt(current, s.inlineOpenChar, s.inlineCloseChar)
- if !plan.inlinePrompt &amp;&amp; !s.isTriggerEvent(p, current) </span><span class="cov6" title="9">{
+ _, _, openChar, closeChar := s.inlineMarkers()
+ plan.inlinePrompt = lineHasInlinePrompt(current, openChar, closeChar)
+ if !plan.inlinePrompt &amp;&amp; !s.isTriggerEvent(p, current) </span><span class="cov6" title="8">{
logging.Logf("lsp ", "%scompletion skip=no-trigger line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase)
return plan, []CompletionItem{}, true
}</span>
- <span class="cov6" title="9">if s.shouldSuppressForChatTriggerEOL(current, p) </span><span class="cov0" title="0">{
+ <span class="cov6" title="10">if s.shouldSuppressForChatTriggerEOL(current, p) </span><span class="cov0" title="0">{
return plan, []CompletionItem{}, true
}</span>
- <span class="cov6" title="9">plan.inParams = inParamList(current, p.Position.Character)
+ <span class="cov6" title="10">plan.inParams = inParamList(current, p.Position.Character)
plan.manualInvoke = parseManualInvoke(p.Context)
plan.cacheKey = s.completionCacheKey(p, above, current, below, funcCtx, plan.inParams, hasExtra, extraText)
if cleaned, ok := s.completionCacheGet(plan.cacheKey); ok &amp;&amp; strings.TrimSpace(cleaned) != "" </span><span class="cov1" title="1">{
@@ -5244,108 +5298,107 @@ func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below
logging.AnsiGreen, logging.PreviewForLog(cleaned), logging.AnsiBase)
return plan, s.makeCompletionItems(cleaned, plan.inParams, current, p, docStr), true
}</span>
- <span class="cov6" title="8">if isBareDoubleOpen(current, s.inlineOpenChar, s.inlineCloseChar) || isBareDoubleOpen(below, s.inlineOpenChar, s.inlineCloseChar) </span><span class="cov0" title="0">{
+ <span class="cov6" title="9">if isBareDoubleOpen(current, openChar, closeChar) || isBareDoubleOpen(below, openChar, closeChar) </span><span class="cov0" title="0">{
logging.Logf("lsp ", "%scompletion skip=empty-double-semicolon line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase)
return plan, []CompletionItem{}, true
}</span>
- <span class="cov6" title="8">if !plan.inParams &amp;&amp; !s.prefixHeuristicAllows(plan.inlinePrompt, current, p, plan.manualInvoke) </span><span class="cov0" title="0">{
+ <span class="cov6" title="9">if !plan.inParams &amp;&amp; !s.prefixHeuristicAllows(plan.inlinePrompt, current, p, plan.manualInvoke) </span><span class="cov0" title="0">{
logging.Logf("lsp ", "%scompletion skip=short-prefix line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase)
return plan, []CompletionItem{}, true
}</span>
- <span class="cov6" title="8">return plan, nil, false</span>
+ <span class="cov6" title="9">return plan, nil, false</span>
}
-func (s *Server) executeChatCompletion(ctx context.Context, plan completionPlan) ([]CompletionItem, bool) <span class="cov5" title="7">{
+func (s *Server) executeChatCompletion(ctx context.Context, plan completionPlan) ([]CompletionItem, bool) <span class="cov6" title="8">{
messages := s.buildCompletionMessages(plan.inlinePrompt, plan.hasExtra, plan.extraText, plan.inParams, plan.params, plan.above, plan.current, plan.below, plan.funcCtx)
sentSize := 0
- for _, m := range messages </span><span class="cov7" title="14">{
+ for _, m := range messages </span><span class="cov7" title="16">{
sentSize += len(m.Content)
}</span>
- <span class="cov5" title="7">s.incSentCounters(sentSize)
- opts := []llm.RequestOption{llm.WithMaxTokens(s.maxTokens)}
- if s.codingTemperature != nil </span><span class="cov0" title="0">{
- opts = append(opts, llm.WithTemperature(*s.codingTemperature))
- }</span>
- <span class="cov5" title="7">s.waitForDebounce(ctx)
+ <span class="cov6" title="8">s.incSentCounters(sentSize)
+ opts := s.llmRequestOpts()
+ s.waitForDebounce(ctx)
if !s.waitForThrottle(ctx) </span><span class="cov0" title="0">{
return nil, false
}</span>
- <span class="cov5" title="7">if s.llmClient == nil </span><span class="cov0" title="0">{
+ <span class="cov6" title="8">client := s.currentLLMClient()
+ if client == nil </span><span class="cov0" title="0">{
return nil, false
}</span>
- <span class="cov5" title="7">logging.Logf("lsp ", "completion llm=requesting model=%s", s.llmClient.DefaultModel())
- text, err := s.llmClient.Chat(ctx, messages, opts...)
+ <span class="cov6" title="8">logging.Logf("lsp ", "completion llm=requesting model=%s", client.DefaultModel())
+ text, err := client.Chat(ctx, messages, opts...)
if err != nil </span><span class="cov0" title="0">{
logging.Logf("lsp ", "llm completion error: %v", err)
s.logLLMStats()
return nil, false
}</span>
- <span class="cov5" title="7">s.incRecvCounters(len(text))
+ <span class="cov6" title="8">s.incRecvCounters(len(text))
s.logLLMStats()
trimmed := strings.TrimSpace(text)
cleaned := s.postProcessCompletion(trimmed, plan.current[:plan.params.Position.Character], plan.current)
if cleaned == "" </span><span class="cov0" title="0">{
return nil, false
}</span>
- <span class="cov5" title="7">s.completionCachePut(plan.cacheKey, cleaned)
+ <span class="cov6" title="8">s.completionCachePut(plan.cacheKey, cleaned)
items := s.makeCompletionItems(cleaned, plan.inParams, plan.current, plan.params, plan.docStr)
return items, true</span>
}
// parseManualInvoke inspects the LSP completion context and reports whether the user manually invoked completion.
-func parseManualInvoke(ctx any) bool <span class="cov6" title="10">{
+func parseManualInvoke(ctx any) bool <span class="cov6" title="11">{
if ctx == nil </span><span class="cov4" title="5">{
return false
}</span>
- <span class="cov4" title="5">var c struct {
+ <span class="cov5" title="6">var c struct {
TriggerKind int `json:"triggerKind"`
}
if raw, ok := ctx.(json.RawMessage); ok </span><span class="cov4" title="5">{
_ = json.Unmarshal(raw, &amp;c)
- }</span> else<span class="cov0" title="0"> {
+ }</span> else<span class="cov1" title="1"> {
b, _ := json.Marshal(ctx)
_ = json.Unmarshal(b, &amp;c)
}</span>
- <span class="cov4" title="5">return c.TriggerKind == 1</span>
+ <span class="cov5" title="6">return c.TriggerKind == 1</span>
}
// shouldSuppressForChatTriggerEOL returns true when a chat trigger like "&gt;" follows ?, !, :, or ; at EOL.
-func (s *Server) shouldSuppressForChatTriggerEOL(current string, p CompletionParams) bool <span class="cov7" title="14">{
+func (s *Server) shouldSuppressForChatTriggerEOL(current string, p CompletionParams) bool <span class="cov7" title="15">{
t := strings.TrimRight(current, " \t")
- if s.chatSuffix == "" </span><span class="cov1" title="1">{
+ suffix, prefixes, _ := s.chatConfig()
+ if suffix == "" </span><span class="cov1" title="1">{
return false
}</span>
- <span class="cov7" title="13">if strings.HasSuffix(t, s.chatSuffix) </span><span class="cov4" title="4">{
- if len(t) &lt; len(s.chatSuffix)+1 </span><span class="cov0" title="0">{
+ <span class="cov7" title="14">if strings.HasSuffix(t, suffix) </span><span class="cov4" title="4">{
+ if len(t) &lt; len(suffix)+1 </span><span class="cov0" title="0">{
return false
}</span>
- <span class="cov4" title="4">prev := string(t[len(t)-len(s.chatSuffix)-1])
- for _, pf := range s.chatPrefixes </span><span class="cov6" title="10">{
+ <span class="cov4" title="4">prev := string(t[len(t)-len(suffix)-1])
+ for _, pf := range prefixes </span><span class="cov6" title="10">{
if prev == pf </span><span class="cov2" title="2">{
logging.Logf("lsp ", "completion skip=chat-trigger-eol uri=%s line=%d", p.TextDocument.URI, p.Position.Line)
return true
}</span>
}
}
- <span class="cov6" title="11">return false</span>
+ <span class="cov7" title="12">return false</span>
}
// prefixHeuristicAllows applies minimal prefix rules unless inlinePrompt or structural triggers apply.
-func (s *Server) prefixHeuristicAllows(inlinePrompt bool, current string, p CompletionParams, manualInvoke bool) bool <span class="cov7" title="13">{
+func (s *Server) prefixHeuristicAllows(inlinePrompt bool, current string, p CompletionParams, manualInvoke bool) bool <span class="cov7" title="14">{
// Determine the effective cursor index within current line, clamped, and
// skip over trailing spaces/tabs to support cases like "type Matrix| ".
idx := p.Position.Character
if idx &gt; len(current) </span><span class="cov0" title="0">{
idx = len(current)
}</span>
- <span class="cov7" title="13">allowNoPrefix := inlinePrompt
- if idx &gt; 0 </span><span class="cov6" title="11">{
+ <span class="cov7" title="14">allowNoPrefix := inlinePrompt
+ if idx &gt; 0 </span><span class="cov7" title="12">{
ch := current[idx-1]
- if ch == '.' || ch == ':' || ch == '/' || ch == '_' || ch == ')' </span><span class="cov4" title="4">{
+ if ch == '.' || ch == ':' || ch == '/' || ch == '_' || ch == ')' </span><span class="cov4" title="5">{
allowNoPrefix = true
}</span>
}
- <span class="cov7" title="13">if allowNoPrefix </span><span class="cov5" title="6">{
+ <span class="cov7" title="14">if allowNoPrefix </span><span class="cov5" title="7">{
return true
}</span>
// Walk left over whitespace
@@ -5360,35 +5413,40 @@ func (s *Server) prefixHeuristicAllows(inlinePrompt bool, current string, p Comp
}
<span class="cov5" title="7">start := computeWordStart(current, j)
min := 1
- if manualInvoke &amp;&amp; s.manualInvokeMinPrefix &gt;= 0 </span><span class="cov4" title="5">{
- min = s.manualInvokeMinPrefix
- }</span>
+ if manualInvoke </span><span class="cov4" title="5">{
+ if v := s.manualInvokeMinPrefix(); v &gt;= 0 </span><span class="cov4" title="5">{
+ min = v
+ }</span>
+ }
<span class="cov5" title="7">return j-start &gt;= min</span>
}
// tryProviderNativeCompletion attempts provider-native completion and returns items when successful.
-func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams, above, below, funcCtx, docStr string, hasExtra bool, extraText string, inParams bool) ([]CompletionItem, bool) <span class="cov6" title="11">{
- cc, ok := s.llmClient.(llm.CodeCompleter)
+func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams, above, below, funcCtx, docStr string, hasExtra bool, extraText string, inParams bool) ([]CompletionItem, bool) <span class="cov7" title="12">{
+ client := s.currentLLMClient()
+ cc, ok := client.(llm.CodeCompleter)
if !ok </span><span class="cov5" title="6">{
return nil, false
}</span>
- <span class="cov4" title="5">before, after := s.docBeforeAfter(p.TextDocument.URI, p.Position)
+ <span class="cov5" title="6">before, after := s.docBeforeAfter(p.TextDocument.URI, p.Position)
path := strings.TrimPrefix(p.TextDocument.URI, "file://")
// Build provider-native prompt from template
- prompt := renderTemplate(s.promptNativeCompletion, map[string]string{
+ cfg := s.currentConfig()
+ _, _, openChar, closeChar := s.inlineMarkers()
+ prompt := renderTemplate(cfg.PromptNativeCompletion, map[string]string{
"path": path,
"before": before,
})
lang := ""
temp := 0.0
- if s.codingTemperature != nil </span><span class="cov0" title="0">{
- temp = *s.codingTemperature
+ if cfg.CodingTemperature != nil </span><span class="cov0" title="0">{
+ temp = *cfg.CodingTemperature
}</span>
- <span class="cov4" title="5">prov := ""
- if s.llmClient != nil </span><span class="cov4" title="5">{
- prov = s.llmClient.Name()
+ <span class="cov5" title="6">prov := ""
+ if client != nil </span><span class="cov5" title="6">{
+ prov = client.Name()
}</span>
- <span class="cov4" title="5">logging.Logf("lsp ", "completion path=codex provider=%s uri=%s", prov, path)
+ <span class="cov5" title="6">logging.Logf("lsp ", "completion path=codex provider=%s uri=%s", prov, path)
ctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel2()
@@ -5398,15 +5456,15 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams,
return nil, false
}</span>
// Count approximate payload sizes: prompt+after sent; first suggestion received
- <span class="cov4" title="5">sentBytes := len(prompt) + len(after)
+ <span class="cov5" title="6">sentBytes := len(prompt) + len(after)
suggestions, err := cc.CodeCompletion(ctx2, prompt, after, 1, lang, temp)
if err == nil &amp;&amp; len(suggestions) &gt; 0 </span><span class="cov4" title="4">{
// Update counters and heartbeat
s.incSentCounters(sentBytes)
s.incRecvCounters(len(suggestions[0]))
// Contribute to global stats (provider-native path)
- if s.llmClient != nil </span><span class="cov4" title="4">{
- _ = stats.Update(ctx2, s.llmClient.Name(), s.llmClient.DefaultModel(), sentBytes, len(suggestions[0]))
+ if client != nil </span><span class="cov4" title="4">{
+ _ = stats.Update(ctx2, client.Name(), client.DefaultModel(), sentBytes, len(suggestions[0]))
}</span>
<span class="cov4" title="4">s.logLLMStats()
cleaned := strings.TrimSpace(suggestions[0])
@@ -5415,7 +5473,7 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams,
if cleaned != "" </span><span class="cov4" title="4">{
cleaned = stripDuplicateGeneralPrefix(current[:p.Position.Character], cleaned)
}</span>
- <span class="cov4" title="4">if cleaned != "" &amp;&amp; hasDoubleOpenTrigger(current, s.inlineOpenChar, s.inlineCloseChar) </span><span class="cov1" title="1">{
+ <span class="cov4" title="4">if cleaned != "" &amp;&amp; hasDoubleOpenTrigger(current, openChar, closeChar) </span><span class="cov1" title="1">{
indent := leadingIndent(current)
if indent != "" </span><span class="cov1" title="1">{
cleaned = applyIndent(indent, cleaned)
@@ -5427,20 +5485,20 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams,
return s.makeCompletionItems(cleaned, inParams, current, p, docStr), true
}</span>
}
- } else<span class="cov1" title="1"> if err != nil </span><span class="cov1" title="1">{
+ } else<span class="cov2" title="2"> if err != nil </span><span class="cov2" title="2">{
logging.Logf("lsp ", "completion path=codex error=%v (falling back to chat)", err)
// Still emit a heartbeat for visibility, even on error
s.incSentCounters(sentBytes)
s.logLLMStats()
}</span>
- <span class="cov1" title="1">return nil, false</span>
+ <span class="cov2" title="2">return nil, false</span>
}
// waitForDebounce sleeps until there has been no input activity for at least
// completionDebounce. If debounce is zero or ctx is done, it returns promptly.
-func (s *Server) waitForDebounce(ctx context.Context) <span class="cov10" title="39">{
- d := s.completionDebounce
- if d &lt;= 0 </span><span class="cov9" title="37">{
+func (s *Server) waitForDebounce(ctx context.Context) <span class="cov10" title="41">{
+ d := s.completionDebounce()
+ if d &lt;= 0 </span><span class="cov9" title="39">{
return
}</span>
<span class="cov2" title="2">for </span><span class="cov4" title="4">{
@@ -5468,9 +5526,9 @@ func (s *Server) waitForDebounce(ctx context.Context) <span class="cov10" title=
// waitForThrottle enforces a minimum spacing between LLM calls. Returns false
// if the context is canceled while waiting.
-func (s *Server) waitForThrottle(ctx context.Context) bool <span class="cov10" title="39">{
- interval := s.throttleInterval
- if interval &lt;= 0 </span><span class="cov9" title="36">{
+func (s *Server) waitForThrottle(ctx context.Context) bool <span class="cov10" title="41">{
+ interval := s.completionThrottle()
+ if interval &lt;= 0 </span><span class="cov9" title="38">{
return true
}</span>
<span class="cov3" title="3">var wait time.Duration
@@ -5499,8 +5557,7 @@ func (s *Server) waitForThrottle(ctx context.Context) bool <span class="cov10" t
}
// buildCompletionMessages constructs the LLM messages for completion.
-func (s *Server) buildCompletionMessages(inlinePrompt, hasExtra bool, extraText string, inParams bool, p CompletionParams, above, current, below, funcCtx string) []llm.Message <span class="cov7" title="13">{
- // Vars for templates
+func (s *Server) buildCompletionMessages(inlinePrompt, hasExtra bool, extraText string, inParams bool, p CompletionParams, above, current, below, funcCtx string) []llm.Message <span class="cov7" title="14">{
vars := map[string]string{
"file": p.TextDocument.URI,
"function": funcCtx,
@@ -5509,51 +5566,53 @@ func (s *Server) buildCompletionMessages(inlinePrompt, hasExtra bool, extraText
"below": below,
"char": fmt.Sprintf("%d", p.Position.Character),
}
- sys := s.promptCompSysGeneral
- userTpl := s.promptCompUserGeneral
+ cfg := s.currentConfig()
+ sys := cfg.PromptCompletionSystemGeneral
+ userTpl := cfg.PromptCompletionUserGeneral
if inParams </span><span class="cov2" title="2">{
- sys = s.promptCompSysParams
- userTpl = s.promptCompUserParams
+ sys = cfg.PromptCompletionSystemParams
+ userTpl = cfg.PromptCompletionUserParams
}</span>
- <span class="cov7" title="13">if inlinePrompt &amp;&amp; strings.TrimSpace(s.promptCompSysInline) != "" </span><span class="cov1" title="1">{
- sys = s.promptCompSysInline
+ <span class="cov7" title="14">if inlinePrompt &amp;&amp; strings.TrimSpace(cfg.PromptCompletionSystemInline) != "" </span><span class="cov2" title="2">{
+ sys = cfg.PromptCompletionSystemInline
}</span>
- <span class="cov7" title="13">user := renderTemplate(userTpl, vars)
+ <span class="cov7" title="14">user := renderTemplate(userTpl, vars)
messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
if hasExtra &amp;&amp; strings.TrimSpace(extraText) != "" </span><span class="cov1" title="1">{
- extra := renderTemplate(s.promptCompExtraHeader, map[string]string{"context": extraText})
+ extra := renderTemplate(cfg.PromptCompletionExtraHeader, map[string]string{"context": extraText})
if strings.TrimSpace(extra) == "" </span><span class="cov0" title="0">{
extra = extraText
}</span>
<span class="cov1" title="1">messages = append(messages, llm.Message{Role: "user", Content: extra})</span>
}
- <span class="cov7" title="13">return messages</span>
+ <span class="cov7" title="14">return messages</span>
}
// postProcessCompletion normalizes and deduplicates completion text and applies indentation rules.
-func (s *Server) postProcessCompletion(text string, leftOfCursor string, currentLine string) string <span class="cov6" title="10">{
+func (s *Server) postProcessCompletion(text string, leftOfCursor string, currentLine string) string <span class="cov6" title="11">{
cleaned := stripCodeFences(text)
if cleaned != "" &amp;&amp; strings.ContainsRune(cleaned, '`') </span><span class="cov0" title="0">{
if inline := stripInlineCodeSpan(cleaned); strings.TrimSpace(inline) != "" </span><span class="cov0" title="0">{
cleaned = inline
}</span>
}
- <span class="cov6" title="10">if cleaned != "" </span><span class="cov6" title="10">{
+ <span class="cov6" title="11">if cleaned != "" </span><span class="cov6" title="11">{
cleaned = stripDuplicateAssignmentPrefix(leftOfCursor, cleaned)
}</span>
- <span class="cov6" title="10">if cleaned != "" </span><span class="cov6" title="10">{
+ <span class="cov6" title="11">if cleaned != "" </span><span class="cov6" title="11">{
cleaned = stripDuplicateGeneralPrefix(leftOfCursor, cleaned)
}</span>
- <span class="cov6" title="10">if cleaned != "" &amp;&amp; hasDoubleOpenTrigger(currentLine, s.inlineOpenChar, s.inlineCloseChar) </span><span class="cov1" title="1">{
+ <span class="cov6" title="11">_, _, openChar, closeChar := s.inlineMarkers()
+ if cleaned != "" &amp;&amp; hasDoubleOpenTrigger(currentLine, openChar, closeChar) </span><span class="cov1" title="1">{
if indent := leadingIndent(currentLine); indent != "" </span><span class="cov1" title="1">{
cleaned = applyIndent(indent, cleaned)
}</span>
}
- <span class="cov6" title="10">return cleaned</span>
+ <span class="cov6" title="11">return cleaned</span>
}
</pre>
- <pre class="file" id="file27" style="display: none">// Summary: Document open/change/close and in-editor chat handlers split out of handlers.go.
+ <pre class="file" id="file28" style="display: none">// Summary: Document open/change/close and in-editor chat handlers split out of handlers.go.
package lsp
import (
@@ -5597,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="cov6" title="7">{
+func (s *Server) docBeforeAfter(uri string, pos Position) (string, string) <span class="cov7" title="8">{
d := s.getDocument(uri)
if d == nil </span><span class="cov5" title="4">{
return "", ""
}</span>
// Clamp indices
- <span class="cov4" title="3">line := pos.Line
+ <span class="cov5" title="4">line := pos.Line
if line &lt; 0 </span><span class="cov0" title="0">{
line = 0
}</span>
- <span class="cov4" title="3">if line &gt;= len(d.lines) </span><span class="cov1" title="1">{
+ <span class="cov5" title="4">if line &gt;= len(d.lines) </span><span class="cov1" title="1">{
line = len(d.lines) - 1
}</span>
- <span class="cov4" title="3">col := pos.Character
+ <span class="cov5" title="4">col := pos.Character
if col &lt; 0 </span><span class="cov0" title="0">{
col = 0
}</span>
- <span class="cov4" title="3">if col &gt; len(d.lines[line]) </span><span class="cov1" title="1">{
+ <span class="cov5" title="4">if col &gt; len(d.lines[line]) </span><span class="cov1" title="1">{
col = len(d.lines[line])
}</span>
// Build before
- <span class="cov4" title="3">var b strings.Builder
- for i := 0; i &lt; line; i++ </span><span class="cov4" title="3">{
+ <span class="cov5" title="4">var b strings.Builder
+ for i := 0; i &lt; line; i++ </span><span class="cov5" title="5">{
b.WriteString(d.lines[i])
b.WriteByte('\n')
}</span>
- <span class="cov4" title="3">b.WriteString(d.lines[line][:col])
+ <span class="cov5" 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 &lt; len(d.lines); i++ </span><span class="cov3" title="2">{
+ for i := line + 1; i &lt; len(d.lines); i++ </span><span class="cov5" title="4">{
a.WriteByte('\n')
a.WriteString(d.lines[i])
}</span>
- <span class="cov4" title="3">return before, a.String()</span>
+ <span class="cov5" title="4">return before, a.String()</span>
}
// --- in-editor chat (";C ...") ---
@@ -5641,32 +5700,30 @@ func (s *Server) docBeforeAfter(uri string, pos Position) (string, string) <span
// a new trigger pair (e.g., "?&gt;" ",&gt;" ":&gt;" ";&gt;") at EOL and inserts the LLM
// reply below.
func (s *Server) detectAndHandleChat(uri string) <span class="cov7" title="10">{
- if s.llmClient == nil </span><span class="cov1" title="1">{
- return
- }</span>
- <span class="cov7" title="9">d := s.getDocument(uri)
+ d := s.getDocument(uri)
if d == nil || len(d.lines) == 0 </span><span class="cov0" title="0">{
return
}</span>
- <span class="cov7" title="9">for i, raw := range d.lines </span><span class="cov10" title="20">{
+ <span class="cov7" title="10">suffix, prefixes, _ := s.chatConfig()
+ for i, raw := range d.lines </span><span class="cov10" title="22">{
// Find last non-space character index
j := len(raw) - 1
- for j &gt;= 0 </span><span class="cov9" title="18">{
+ for j &gt;= 0 </span><span class="cov9" title="19">{
if raw[j] == ' ' || raw[j] == '\t' </span><span class="cov0" title="0">{
j--
continue</span>
}
- <span class="cov9" title="18">break</span>
+ <span class="cov9" title="19">break</span>
}
- <span class="cov10" title="20">if j &lt; 0 </span><span class="cov3" title="2">{
+ <span class="cov10" title="22">if j &lt; 0 </span><span class="cov4" title="3">{
continue</span>
}
// Check suffix/prefix according to configuration
- <span class="cov9" title="18">if s.chatSuffix == "" </span><span class="cov0" title="0">{
+ <span class="cov9" title="19">if suffix == "" </span><span class="cov0" title="0">{
continue</span>
}
// Last non-space must equal suffix
- <span class="cov9" title="18">if string(raw[j]) != s.chatSuffix </span><span class="cov7" title="9">{
+ <span class="cov9" title="19">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
@@ -5675,7 +5732,7 @@ func (s *Server) detectAndHandleChat(uri string) <span class="cov7" title="10">{
}
<span class="cov7" title="9">prev := string(raw[j-1])
isTrigger := false
- for _, pfx := range s.chatPrefixes </span><span class="cov7" title="9">{
+ for _, pfx := range prefixes </span><span class="cov7" title="9">{
if prev == pfx </span><span class="cov7" title="9">{
isTrigger = true
break</span>
@@ -5693,7 +5750,7 @@ func (s *Server) detectAndHandleChat(uri string) <span class="cov7" title="10">{
continue</span>
}
// Derive prompt by removing only the trailing '&gt;'
- <span class="cov7" title="8">removeCount := len(s.chatSuffix)
+ <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">{
@@ -5701,14 +5758,28 @@ func (s *Server) detectAndHandleChat(uri string) <span class="cov7" title="10">{
}
<span class="cov7" title="8">lineIdx := i
lastIdx := j
- go func(prompt string, remove int) </span><span class="cov7" title="8">{
+ if resp, ok := s.chatCommandResponse(uri, lineIdx, prompt); ok </span><span class="cov0" title="0">{
+ msg := strings.TrimSpace(resp.message)
+ if msg != "" </span><span class="cov0" title="0">{
+ s.applyChatEdits(uri, lineIdx, lastIdx, removeCount, "&gt; "+msg)
+ }</span>
+ <span class="cov0" title="0">return</span>
+ }
+ <span class="cov7" 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">{
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
// Build messages with history and context_mode aware extras.
pos := Position{Line: lineIdx, Character: lastIdx + 1}
msgs := s.buildChatMessages(uri, pos, prompt)
opts := s.llmRequestOpts()
- logging.Logf("lsp ", "chat llm=requesting model=%s", s.llmClient.DefaultModel())
+ client := s.currentLLMClient()
+ 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())
text, err := s.chatWithStats(ctx, msgs, opts...)
if err != nil </span><span class="cov0" title="0">{
logging.Logf("lsp ", "chat llm error: %v", err)
@@ -5802,24 +5873,25 @@ 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="cov7" title="8">{
+func (s *Server) stripTrailingTrigger(sx string) string <span class="cov9" title="16">{
trim := strings.TrimRight(sx, " \t")
if len(trim) == 0 </span><span class="cov0" title="0">{
return sx
}</span>
- <span class="cov7" title="8">if len(trim) &gt;= 2 &amp;&amp; s.chatSuffixChar != 0 &amp;&amp; trim[len(trim)-1] == s.chatSuffixChar </span><span class="cov5" title="5">{
+ <span class="cov9" title="16">_, prefixes, suffixChar := s.chatConfig()
+ if len(trim) &gt;= 2 &amp;&amp; suffixChar != 0 &amp;&amp; trim[len(trim)-1] == suffixChar </span><span class="cov5" title="5">{
prev := string(trim[len(trim)-2])
- for _, pf := range s.chatPrefixes </span><span class="cov8" title="11">{
+ for _, pf := range prefixes </span><span class="cov7" title="11">{
if prev == pf </span><span class="cov5" title="5">{
return strings.TrimRight(trim[:len(trim)-1], " \t")
}</span>
}
}
- <span class="cov4" title="3">last := trim[len(trim)-1]
+ <span class="cov7" title="11">last := trim[len(trim)-1]
switch last </span>{
- case '?', '!', ':':<span class="cov1" title="1">
+ case '?', '!', ':':<span class="cov7" title="8">
return strings.TrimRight(trim[:len(trim)-1], " \t")</span>
- default:<span class="cov3" title="2">
+ default:<span class="cov4" title="3">
return sx</span>
}
}
@@ -5830,7 +5902,8 @@ func (s *Server) stripTrailingTrigger(sx string) string <span class="cov7" title
// - 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">{
// Base system and history
- sys := s.promptChatSystem
+ cfg := s.currentConfig()
+ sys := cfg.PromptChatSystem
// Determine line index for history from position
lineIdx := pos.Line
history := s.buildChatHistory(uri, lineIdx, prompt)
@@ -5840,7 +5913,7 @@ func (s *Server) buildChatMessages(uri string, pos Position, prompt string) []ll
newFunc := s.isDefiningNewFunction(uri, pos)
if extra, has := s.buildAdditionalContext(newFunc, uri, pos); has &amp;&amp; strings.TrimSpace(extra) != "" </span><span class="cov4" title="3">{
// Reuse completion's extra header template to avoid duplication
- header := renderTemplate(s.promptCompExtraHeader, map[string]string{"context": extra})
+ header := renderTemplate(cfg.PromptCompletionExtraHeader, map[string]string{"context": extra})
if strings.TrimSpace(header) == "" </span><span class="cov0" title="0">{
header = extra
}</span>
@@ -5862,7 +5935,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="cov8" title="11">{
+func (s *Server) nextReqID() json.RawMessage <span class="cov7" title="11">{
s.mu.Lock()
s.nextID++
idNum := s.nextID
@@ -5899,7 +5972,7 @@ func (s *Server) deferShowDocument(uri string, sel Range) <span class="cov1" tit
}
</pre>
- <pre class="file" id="file28" style="display: none">// Summary: ExecuteCommand handler to support post-edit navigation (jump to generated test).
+ <pre class="file" id="file29" style="display: none">// Summary: ExecuteCommand handler to support post-edit navigation (jump to generated test).
package lsp
import (
@@ -5935,7 +6008,7 @@ func (s *Server) handleExecuteCommand(req Request) <span class="cov8" title="1">
}
</pre>
- <pre class="file" id="file29" style="display: none">// Summary: Initialization and lifecycle handlers split from handlers.go.
+ <pre class="file" id="file30" style="display: none">// Summary: Initialization and lifecycle handlers split from handlers.go.
package lsp
import (
@@ -5947,16 +6020,17 @@ import (
)
func (s *Server) handleInitialize(req Request) <span class="cov10" title="2">{
+ client := s.currentLLMClient()
version := internal.Version
- if s.llmClient != nil </span><span class="cov0" title="0">{
- version = version + " [" + s.llmClient.Name() + ":" + s.llmClient.DefaultModel() + "]"
+ if client != nil </span><span class="cov0" title="0">{
+ version = version + " [" + client.Name() + ":" + client.DefaultModel() + "]"
}</span>
<span class="cov10" title="2">res := InitializeResult{
Capabilities: ServerCapabilities{
TextDocumentSync: 1, // 1 = TextDocumentSyncKindFull
CompletionProvider: &amp;CompletionOptions{
ResolveProvider: false,
- TriggerCharacters: s.triggerChars,
+ TriggerCharacters: s.triggerCharacters(),
},
CodeActionProvider: CodeActionOptions{ResolveProvider: true},
},
@@ -5968,8 +6042,8 @@ func (s *Server) handleInitialize(req Request) <span class="cov10" title="2">{
func (s *Server) handleInitialized() <span class="cov1" title="1">{
logging.Logf("lsp ", "client initialized")
// Emit an initial tmux heartbeat with provider/model
- if s.llmClient != nil </span><span class="cov0" title="0">{
- _ = tmx.SetStatus(tmx.FormatLLMStartStatus(s.llmClient.Name(), s.llmClient.DefaultModel()))
+ if client := s.currentLLMClient(); client != nil </span><span class="cov0" title="0">{
+ _ = tmx.SetStatus(tmx.FormatLLMStartStatus(client.Name(), client.DefaultModel()))
}</span>
}
@@ -5983,11 +6057,12 @@ func (s *Server) handleExit() <span class="cov0" title="0">{
}</span>
</pre>
- <pre class="file" id="file30" style="display: none">// Summary: Generic LSP helpers shared across handlers (LLM opts, prompts, text utils, counters).
+ <pre class="file" id="file31" style="display: none">// Summary: Generic LSP helpers shared across handlers (LLM opts, prompts, text utils, counters).
package lsp
import (
"context"
+ "fmt"
"strings"
"time"
@@ -5999,83 +6074,88 @@ import (
)
// llmRequestOpts builds request options from server settings.
-func (s *Server) llmRequestOpts() []llm.RequestOption <span class="cov7" title="27">{
- opts := []llm.RequestOption{llm.WithMaxTokens(s.maxTokens)}
- if s.codingTemperature != nil </span><span class="cov1" title="1">{
- temp := *s.codingTemperature
- if s.llmClient != nil </span><span class="cov1" title="1">{
- prov := strings.ToLower(strings.TrimSpace(s.llmClient.Name()))
- model := strings.ToLower(strings.TrimSpace(s.llmClient.DefaultModel()))
+func (s *Server) llmRequestOpts() []llm.RequestOption <span class="cov7" title="35">{
+ maxTokens := s.maxTokens()
+ client := s.currentLLMClient()
+ tempPtr := s.codingTemperature()
+ opts := []llm.RequestOption{llm.WithMaxTokens(maxTokens)}
+ if tempPtr != nil </span><span class="cov1" title="1">{
+ temp := *tempPtr
+ if client != nil </span><span class="cov1" title="1">{
+ prov := strings.ToLower(strings.TrimSpace(client.Name()))
+ model := strings.ToLower(strings.TrimSpace(client.DefaultModel()))
if prov == "openai" &amp;&amp; strings.HasPrefix(model, "gpt-5") </span><span class="cov1" title="1">{
temp = 1.0
}</span>
}
<span class="cov1" title="1">opts = append(opts, llm.WithTemperature(temp))</span>
}
- <span class="cov7" title="27">return opts</span>
+ <span class="cov7" title="35">return opts</span>
}
// small helpers for LLM traffic stats
-func (s *Server) incSentCounters(n int) <span class="cov8" title="39">{
+func (s *Server) incSentCounters(n int) <span class="cov8" title="41">{
s.mu.Lock()
s.llmReqTotal++
s.llmSentBytesTotal += int64(n)
s.mu.Unlock()
}</span>
-func (s *Server) incRecvCounters(n int) <span class="cov8" title="37">{
+func (s *Server) incRecvCounters(n int) <span class="cov8" title="38">{
s.mu.Lock()
s.llmRespTotal++
s.llmRespBytesTotal += int64(n)
s.mu.Unlock()
}</span>
-func (s *Server) logLLMStats() <span class="cov8" title="39">{
+func (s *Server) logLLMStats() <span class="cov8" title="41">{
s.mu.RLock()
avgSent := int64(0)
- if s.llmReqTotal &gt; 0 </span><span class="cov8" title="39">{
+ if s.llmReqTotal &gt; 0 </span><span class="cov8" title="41">{
avgSent = s.llmSentBytesTotal / s.llmReqTotal
}</span>
- <span class="cov8" title="39">avgRecv := int64(0)
- if s.llmRespTotal &gt; 0 </span><span class="cov8" title="37">{
+ <span class="cov8" title="41">avgRecv := int64(0)
+ if s.llmRespTotal &gt; 0 </span><span class="cov8" title="38">{
avgRecv = s.llmRespBytesTotal / s.llmRespTotal
}</span>
- <span class="cov8" title="39">reqs, sentTot, recvTot := s.llmReqTotal, s.llmSentBytesTotal, s.llmRespBytesTotal
+ <span class="cov8" title="41">reqs, sentTot, recvTot := s.llmReqTotal, s.llmSentBytesTotal, s.llmRespBytesTotal
s.mu.RUnlock()
mins := time.Since(s.startTime).Minutes()
if mins &lt;= 0 </span><span class="cov0" title="0">{
mins = 0.001
}</span>
- <span class="cov8" title="39">rpmLocal := float64(reqs) / mins
+ <span class="cov8" title="41">rpmLocal := float64(reqs) / mins
sentPerMin := float64(sentTot) / mins
recvPerMin := float64(recvTot) / mins
// Log local process counters
logging.Logf("lsp ", "llm stats (local) reqs=%d avg_sent=%d avg_recv=%d sent_total=%d recv_total=%d rpm=%.2f sent_per_min=%.0f recv_per_min=%.0f", reqs, avgSent, avgRecv, sentTot, recvTot, rpmLocal, sentPerMin, recvPerMin)
// Global snapshot for tmux status
snap, err := stats.TakeSnapshot()
- if err == nil &amp;&amp; s.llmClient != nil </span><span class="cov8" title="38">{
- provider := s.llmClient.Name()
- model := s.llmClient.DefaultModel()
- // Per-scope rpm estimated from window
- scopeReqs := int64(0)
- if pe, ok := snap.Providers[provider]; ok </span><span class="cov8" title="38">{
- if mc, ok2 := pe.Models[model]; ok2 </span><span class="cov8" title="38">{
- scopeReqs = mc.Reqs
+ if err == nil </span><span class="cov8" title="41">{
+ if client := s.currentLLMClient(); client != nil </span><span class="cov8" title="40">{
+ provider := client.Name()
+ model := client.DefaultModel()
+ // Per-scope rpm estimated from window
+ scopeReqs := int64(0)
+ if pe, ok := snap.Providers[provider]; ok </span><span class="cov8" title="40">{
+ if mc, ok2 := pe.Models[model]; ok2 </span><span class="cov8" title="40">{
+ scopeReqs = mc.Reqs
+ }</span>
+ }
+ <span class="cov8" title="40">minsWin := snap.Window.Minutes()
+ if minsWin &lt;= 0 </span><span class="cov0" title="0">{
+ minsWin = 0.001
}</span>
+ <span class="cov8" title="40">scopeRPM := float64(scopeReqs) / minsWin
+ status := tmx.FormatGlobalStatusColored(snap.Global.Reqs, snap.RPM, snap.Global.Sent, snap.Global.Recv, provider, model, scopeRPM, scopeReqs, snap.Window)
+ _ = tmx.SetStatus(status)</span>
}
- <span class="cov8" title="38">minsWin := snap.Window.Minutes()
- if minsWin &lt;= 0 </span><span class="cov0" title="0">{
- minsWin = 0.001
- }</span>
- <span class="cov8" title="38">scopeRPM := float64(scopeReqs) / minsWin
- status := tmx.FormatGlobalStatusColored(snap.Global.Reqs, snap.RPM, snap.Global.Sent, snap.Global.Recv, provider, model, scopeRPM, scopeReqs, snap.Window)
- _ = tmx.SetStatus(status)</span>
}
}
// Completion prompt builders and filters
-func inParamList(current string, cursor int) bool <span class="cov5" title="12">{
- if !strings.Contains(current, "func ") </span><span class="cov4" title="6">{
+func inParamList(current string, cursor int) bool <span class="cov5" title="13">{
+ if !strings.Contains(current, "func ") </span><span class="cov4" title="7">{
return false
}</span>
<span class="cov4" title="6">open := strings.Index(current, "(")
@@ -6084,9 +6164,9 @@ func inParamList(current string, cursor int) bool <span class="cov5" title="12">
}
// renderTemplate performs simple {{var}} replacement in a template string.
-func renderTemplate(t string, vars map[string]string) string <span class="cov8" title="40">{ return textutil.RenderTemplate(t, vars) }</span>
+func renderTemplate(t string, vars map[string]string) string <span class="cov8" title="42">{ return textutil.RenderTemplate(t, vars) }</span>
-func computeTextEditAndFilter(cleaned string, inParams bool, current string, p CompletionParams) (*TextEdit, string) <span class="cov6" title="17">{
+func computeTextEditAndFilter(cleaned string, inParams bool, current string, p CompletionParams) (*TextEdit, string) <span class="cov6" title="18">{
if inParams </span><span class="cov3" title="3">{
open := strings.Index(current, "(")
close := strings.Index(current, ")")
@@ -6107,25 +6187,25 @@ func computeTextEditAndFilter(cleaned string, inParams bool, current string, p C
<span class="cov3" title="3">return te, filter</span>
}
}
- <span class="cov6" title="14">startChar := computeWordStart(current, p.Position.Character)
+ <span class="cov6" title="15">startChar := computeWordStart(current, p.Position.Character)
te := &amp;TextEdit{Range: Range{Start: Position{Line: p.Position.Line, Character: startChar}, End: Position{Line: p.Position.Line, Character: p.Position.Character}}, NewText: cleaned}
filter := strings.TrimLeft(current[startChar:p.Position.Character], " \t")
return te, filter</span>
}
-func computeWordStart(current string, at int) int <span class="cov7" title="24">{
+func computeWordStart(current string, at int) int <span class="cov7" title="25">{
if at &gt; len(current) </span><span class="cov0" title="0">{
at = len(current)
}</span>
- <span class="cov7" title="24">for at &gt; 0 </span><span class="cov8" title="49">{
+ <span class="cov7" title="25">for at &gt; 0 </span><span class="cov8" title="50">{
ch := current[at-1]
if (ch &gt;= 'a' &amp;&amp; ch &lt;= 'z') || (ch &gt;= 'A' &amp;&amp; ch &lt;= 'Z') || (ch &gt;= '0' &amp;&amp; ch &lt;= '9') || ch == '_' </span><span class="cov7" title="31">{
at--
continue</span>
}
- <span class="cov6" title="18">break</span>
+ <span class="cov6" title="19">break</span>
}
- <span class="cov7" title="24">return at</span>
+ <span class="cov7" title="25">return at</span>
}
func isIdentChar(ch byte) bool <span class="cov7" title="26">{
@@ -6146,17 +6226,19 @@ func (s *Server) chatWithStats(ctx context.Context, msgs []llm.Message, opts ...
return "", context.Canceled
}</span>
// Perform request
- <span class="cov7" title="26">txt, err := s.llmClient.Chat(ctx, msgs, opts...)
+ <span class="cov7" title="26">client := s.currentLLMClient()
+ if client == nil </span><span class="cov0" title="0">{
+ return "", fmt.Errorf("llm client unavailable")
+ }</span>
+ <span class="cov7" title="26">txt, err := client.Chat(ctx, msgs, opts...)
if err != nil </span><span class="cov1" title="1">{
s.logLLMStats()
return "", err
}</span>
<span class="cov7" title="25">s.incRecvCounters(len(txt))
// Update global stats cache
- if s.llmClient != nil </span><span class="cov7" title="25">{
- _ = stats.Update(ctx, s.llmClient.Name(), s.llmClient.DefaultModel(), sent, len(txt))
- }</span>
- <span class="cov7" title="25">s.logLLMStats()
+ _ = stats.Update(ctx, client.Name(), client.DefaultModel(), sent, len(txt))
+ s.logLLMStats()
return txt, nil</span>
}
@@ -6244,11 +6326,11 @@ func findStrictInlineTag(line string, open, close byte) (string, int, int, bool)
// isBareDoubleSemicolon reports whether the line contains a standalone
// double-semicolon marker with no inline content (";;" possibly with only
// whitespace after it). It explicitly excludes the valid form ";;text;".
-func isBareDoubleOpen(line string, open, close byte) bool <span class="cov6" title="18">{
+func isBareDoubleOpen(line string, open, close byte) bool <span class="cov6" title="20">{
t := strings.TrimSpace(line)
// check for double-open pattern
dbl := string([]byte{open, open})
- if !strings.Contains(t, dbl) </span><span class="cov6" title="16">{
+ if !strings.Contains(t, dbl) </span><span class="cov6" title="18">{
return false
}</span>
<span class="cov2" title="2">if hasDoubleOpenTrigger(t, open, close) </span><span class="cov1" title="1">{
@@ -6264,7 +6346,7 @@ func isBareDoubleOpen(line string, open, close byte) bool <span class="cov6" tit
}
// stripDuplicateAssignmentPrefix removes a duplicated assignment prefix from the suggestion.
-func stripDuplicateAssignmentPrefix(prefixBeforeCursor, suggestion string) string <span class="cov6" title="19">{
+func stripDuplicateAssignmentPrefix(prefixBeforeCursor, suggestion string) string <span class="cov6" title="20">{
s2 := strings.TrimLeft(suggestion, " \t")
// Prefer := if present at end of prefix
if idx := strings.LastIndex(prefixBeforeCursor, ":="); idx &gt;= 0 &amp;&amp; idx+2 &lt;= len(prefixBeforeCursor) </span><span class="cov3" title="4">{
@@ -6282,7 +6364,7 @@ func stripDuplicateAssignmentPrefix(prefixBeforeCursor, suggestion string) strin
}
}
// Fallback to plain '=' if present
- <span class="cov6" title="15">if idx := strings.LastIndex(prefixBeforeCursor, "="); idx &gt;= 0 </span><span class="cov2" title="2">{
+ <span class="cov6" title="16">if idx := strings.LastIndex(prefixBeforeCursor, "="); idx &gt;= 0 </span><span class="cov2" title="2">{
if !(idx &gt; 0 &amp;&amp; prefixBeforeCursor[idx-1] == ':') </span><span class="cov2" title="2">{ // not :=
tail := prefixBeforeCursor[idx+1:]
if strings.TrimSpace(tail) == "" </span><span class="cov2" title="2">{
@@ -6298,21 +6380,21 @@ func stripDuplicateAssignmentPrefix(prefixBeforeCursor, suggestion string) strin
}
}
}
- <span class="cov6" title="13">return suggestion</span>
+ <span class="cov6" title="14">return suggestion</span>
}
// stripDuplicateGeneralPrefix removes any already-typed prefix that the model repeated.
-func stripDuplicateGeneralPrefix(prefixBeforeCursor, suggestion string) string <span class="cov6" title="19">{
+func stripDuplicateGeneralPrefix(prefixBeforeCursor, suggestion string) string <span class="cov6" title="20">{
if suggestion == "" </span><span class="cov0" title="0">{
return suggestion
}</span>
- <span class="cov6" title="19">s := strings.TrimLeft(suggestion, " \t")
+ <span class="cov6" title="20">s := strings.TrimLeft(suggestion, " \t")
p := strings.TrimRight(prefixBeforeCursor, " \t")
if p != "" &amp;&amp; strings.HasPrefix(s, p) </span><span class="cov4" title="5">{
return strings.TrimLeft(s[len(p):], " \t")
}</span>
- <span class="cov6" title="14">for k := len(p) - 1; k &gt; 0; k-- </span><span class="cov10" title="100">{
- if !isIdentBoundary(p[k-1]) </span><span class="cov9" title="77">{
+ <span class="cov6" title="15">for k := len(p) - 1; k &gt; 0; k-- </span><span class="cov10" title="103">{
+ if !isIdentBoundary(p[k-1]) </span><span class="cov9" title="80">{
continue</span>
}
<span class="cov7" title="23">suf := strings.TrimLeft(p[k:], " \t")
@@ -6323,15 +6405,15 @@ func stripDuplicateGeneralPrefix(prefixBeforeCursor, suggestion string) string <
return strings.TrimLeft(s[len(suf):], " \t")
}</span>
}
- <span class="cov6" title="14">return suggestion</span>
+ <span class="cov6" title="15">return suggestion</span>
}
-func isIdentBoundary(ch byte) bool <span class="cov10" title="100">{
+func isIdentBoundary(ch byte) bool <span class="cov10" title="103">{
return !((ch &gt;= 'a' &amp;&amp; ch &lt;= 'z') || (ch &gt;= 'A' &amp;&amp; ch &lt;= 'Z') || (ch &gt;= '0' &amp;&amp; ch &lt;= '9') || ch == '_')
}</span>
// stripCodeFences removes surrounding Markdown code fences from a model response.
-func stripCodeFences(s string) string <span class="cov8" title="44">{ return textutil.StripCodeFences(s) }</span>
+func stripCodeFences(s string) string <span class="cov8" title="45">{ return textutil.StripCodeFences(s) }</span>
// stripInlineCodeSpan returns the contents of the first inline backtick code span if present.
func stripInlineCodeSpan(s string) string <span class="cov5" title="11">{
@@ -6352,12 +6434,12 @@ func stripInlineCodeSpan(s string) string <span class="cov5" title="11">{
}
// labelForCompletion picks a short, readable label for the completion list.
-func labelForCompletion(cleaned, filter string) string <span class="cov6" title="20">{
+func labelForCompletion(cleaned, filter string) string <span class="cov6" title="21">{
label := trimLen(firstLine(cleaned))
if filter != "" &amp;&amp; !strings.HasPrefix(strings.ToLower(label), strings.ToLower(filter)) </span><span class="cov4" title="5">{
return filter
}</span>
- <span class="cov6" title="15">return label</span>
+ <span class="cov6" title="16">return label</span>
}
// extractRangeText returns the exact text within the given document range.
@@ -6406,32 +6488,33 @@ func extractRangeText(d *document, r Range) string <span class="cov4" title="6">
}
// collectPromptRemovalEdits returns edits to remove all inline prompt markers.
-func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit <span class="cov6" title="13">{
+func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit <span class="cov6" title="14">{
d := s.getDocument(uri)
if d == nil || len(d.lines) == 0 </span><span class="cov5" title="11">{
return nil
}</span>
- <span class="cov2" title="2">var edits []TextEdit
- for i, line := range d.lines </span><span class="cov4" title="7">{
- edits = append(edits, promptRemovalEditsForLine(line, i, s.inlineOpenChar, s.inlineCloseChar)...)
+ <span class="cov3" title="3">var edits []TextEdit
+ _, _, openChar, closeChar := s.inlineMarkers()
+ for i, line := range d.lines </span><span class="cov5" title="12">{
+ edits = append(edits, promptRemovalEditsForLine(line, i, openChar, closeChar)...)
}</span>
- <span class="cov2" title="2">return edits</span>
+ <span class="cov3" title="3">return edits</span>
}
-func promptRemovalEditsForLine(line string, lineNum int, open, close byte) []TextEdit <span class="cov5" title="11">{
+func promptRemovalEditsForLine(line string, lineNum int, open, close byte) []TextEdit <span class="cov6" title="16">{
if hasDoubleOpenTrigger(line, open, close) </span><span class="cov3" title="4">{
return []TextEdit{{Range: Range{Start: Position{Line: lineNum, Character: 0}, End: Position{Line: lineNum, Character: len(line)}}, NewText: ""}}
}</span>
- <span class="cov4" title="7">return collectSemicolonMarkers(line, lineNum, open, close)</span>
+ <span class="cov5" title="12">return collectSemicolonMarkers(line, lineNum, open, close)</span>
}
-func hasDoubleOpenTrigger(line string, open, close byte) bool <span class="cov8" title="58">{
+func hasDoubleOpenTrigger(line string, open, close byte) bool <span class="cov9" title="64">{
pos := 0
- for pos &lt; len(line) </span><span class="cov9" title="61">{
+ for pos &lt; len(line) </span><span class="cov9" title="66">{
// look for double-open sequence
dbl := string([]byte{open, open})
j := strings.Index(line[pos:], dbl)
- if j &lt; 0 </span><span class="cov8" title="37">{
+ if j &lt; 0 </span><span class="cov8" title="42">{
return false
}</span>
<span class="cov7" title="24">j += pos
@@ -6456,15 +6539,15 @@ func hasDoubleOpenTrigger(line string, open, close byte) bool <span class="cov8"
}
<span class="cov5" title="10">return true</span>
}
- <span class="cov3" title="3">return false</span>
+ <span class="cov3" title="4">return false</span>
}
-func collectSemicolonMarkers(line string, lineNum int, open, close byte) []TextEdit <span class="cov5" title="9">{
+func collectSemicolonMarkers(line string, lineNum int, open, close byte) []TextEdit <span class="cov6" title="14">{
var edits []TextEdit
startSemi := 0
- for startSemi &lt; len(line) </span><span class="cov6" title="14">{
+ for startSemi &lt; len(line) </span><span class="cov6" title="18">{
j := strings.IndexByte(line[startSemi:], open)
- if j &lt; 0 </span><span class="cov5" title="8">{
+ if j &lt; 0 </span><span class="cov5" title="12">{
break</span>
}
<span class="cov4" title="6">j += startSemi
@@ -6496,11 +6579,11 @@ func collectSemicolonMarkers(line string, lineNum int, open, close byte) []TextE
<span class="cov4" title="6">edits = append(edits, TextEdit{Range: Range{Start: Position{Line: lineNum, Character: j}, End: Position{Line: lineNum, Character: endChar}}, NewText: ""})
startSemi = endChar</span>
}
- <span class="cov5" title="9">return edits</span>
+ <span class="cov6" title="14">return edits</span>
}
</pre>
- <pre class="file" id="file31" style="display: none">// Summary: Minimal LSP server over stdio; manages documents, dispatches requests, and tracks stats.
+ <pre class="file" id="file32" style="display: none">// Summary: Minimal LSP server over stdio; manages documents, dispatches requests, and tracks stats.
package lsp
import (
@@ -6512,29 +6595,26 @@ import (
"sync"
"time"
+ "codeberg.org/snonux/hexai/internal/appconfig"
"codeberg.org/snonux/hexai/internal/llm"
"codeberg.org/snonux/hexai/internal/logging"
+ "codeberg.org/snonux/hexai/internal/runtimeconfig"
)
// Server implements a minimal LSP over stdio.
type Server struct {
- in *bufio.Reader
- out io.Writer
- outMu sync.Mutex
- logger *log.Logger
- exited bool
- mu sync.RWMutex
- docs map[string]*document
- logContext bool
- llmClient llm.Client
- lastInput time.Time
- maxTokens int
- contextMode string
- windowLines int
- maxContextTokens int
- triggerChars []string
- // If set, used as the LSP coding temperature for all LLM calls
- codingTemperature *float64
+ in *bufio.Reader
+ out io.Writer
+ outMu sync.Mutex
+ logger *log.Logger
+ exited bool
+ mu sync.RWMutex
+ docs map[string]*document
+ logContext bool
+ configStore *runtimeconfig.Store
+ cfg appconfig.App
+ llmClient llm.Client
+ lastInput time.Time
// LLM request stats
llmReqTotal int64
llmSentBytesTotal int64
@@ -6545,58 +6625,18 @@ type Server struct {
compCache map[string]string
compCacheOrder []string // most-recent at end; cap ~10
// Outgoing JSON-RPC id counter for server-initiated requests
- nextID int64
- // Minimum identifier chars required for manual invoke to bypass prefix checks
- manualInvokeMinPrefix int
-
- // Debounce and throttle settings
- completionDebounce time.Duration
- throttleInterval time.Duration
- lastLLMCall time.Time
+ nextID int64
+ lastLLMCall time.Time
// Dispatch table for JSON-RPC methods → handler functions
handlers map[string]func(Request)
-
- // Configurable trigger characters
- inlineOpen string
- inlineClose string
- chatSuffix string
- chatPrefixes []string
- inlineOpenChar byte
- inlineCloseChar byte
- chatSuffixChar byte
-
- // Prompt templates
- // Completion
- promptCompSysGeneral string
- promptCompSysParams string
- promptCompSysInline string
- promptCompUserGeneral string
- promptCompUserParams string
- promptCompExtraHeader string
- // Provider-native code completion
- promptNativeCompletion string
- // In-editor chat
- promptChatSystem string
- // Code actions
- promptRewriteSystem string
- promptDiagnosticsSystem string
- promptDocumentSystem string
- promptRewriteUser string
- promptDiagnosticsUser string
- promptDocumentUser string
- promptGoTestSystem string
- promptGoTestUser string
- promptSimplifySystem string
- promptSimplifyUser string
-
- // Custom actions configured by user
- customActions []CustomAction
}
// ServerOptions collects configuration for NewServer to avoid long parameter lists.
type ServerOptions struct {
LogContext bool
+ ConfigStore *runtimeconfig.Store
+ Config *appconfig.App
MaxTokens int
ContextMode string
WindowLines int
@@ -6650,109 +6690,13 @@ type CustomAction struct {
User string // if set, use this user template
}
-func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) *Server <span class="cov10" title="8">{
- s := &amp;Server{in: bufio.NewReader(r), out: w, logger: logger, docs: make(map[string]*document), logContext: opts.LogContext}
- maxTokens := opts.MaxTokens
- if maxTokens &lt;= 0 </span><span class="cov9" title="7">{
- maxTokens = 500
- }</span>
- <span class="cov10" title="8">s.maxTokens = maxTokens
- contextMode := opts.ContextMode
- if contextMode == "" </span><span class="cov9" title="7">{
- contextMode = "file-on-new-func"
- }</span>
- <span class="cov10" title="8">windowLines := opts.WindowLines
- if windowLines &lt;= 0 </span><span class="cov9" title="7">{
- windowLines = 120
- }</span>
- <span class="cov10" title="8">maxContextTokens := opts.MaxContextTokens
- if maxContextTokens &lt;= 0 </span><span class="cov9" title="7">{
- maxContextTokens = 2000
- }</span>
- <span class="cov10" title="8">s.contextMode = contextMode
- s.windowLines = windowLines
- s.maxContextTokens = maxContextTokens
-
+func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) *Server <span class="cov3" title="7">{
+ s := &amp;Server{in: bufio.NewReader(r), out: w, logger: logger, docs: make(map[string]*document), logContext: opts.LogContext, configStore: opts.ConfigStore}
s.startTime = time.Now()
- s.llmClient = opts.Client
- if len(opts.TriggerCharacters) == 0 </span><span class="cov10" title="8">{
- // Defaults (no space to avoid auto-trigger after whitespace)
- s.triggerChars = []string{".", ":", "/", "_", ")", "{"}
- }</span> else<span class="cov0" title="0"> {
- s.triggerChars = append([]string{}, opts.TriggerCharacters...)
- }</span>
- <span class="cov10" title="8">s.codingTemperature = opts.CodingTemperature
s.compCache = make(map[string]string)
- s.manualInvokeMinPrefix = opts.ManualInvokeMinPrefix
- if opts.CompletionDebounceMs &gt; 0 </span><span class="cov1" title="1">{
- s.completionDebounce = time.Duration(opts.CompletionDebounceMs) * time.Millisecond
- }</span>
- <span class="cov10" title="8">if opts.CompletionThrottleMs &gt; 0 </span><span class="cov0" title="0">{
- s.throttleInterval = time.Duration(opts.CompletionThrottleMs) * time.Millisecond
- }</span>
- // Trigger character config (with sane defaults if missing)
- <span class="cov10" title="8">if strings.TrimSpace(opts.InlineOpen) == "" </span><span class="cov8" title="6">{
- s.inlineOpen = "&gt;"
- }</span> else<span class="cov4" title="2"> {
- s.inlineOpen = opts.InlineOpen
- }</span>
- <span class="cov10" title="8">if strings.TrimSpace(opts.InlineClose) == "" </span><span class="cov8" title="6">{
- s.inlineClose = "&gt;"
- }</span> else<span class="cov4" title="2"> {
- s.inlineClose = opts.InlineClose
- }</span>
- <span class="cov10" title="8">if strings.TrimSpace(opts.ChatSuffix) == "" </span><span class="cov7" title="5">{
- s.chatSuffix = "&gt;"
- }</span> else<span class="cov5" title="3"> {
- s.chatSuffix = opts.ChatSuffix
- }</span>
- <span class="cov10" title="8">if len(opts.ChatPrefixes) == 0 </span><span class="cov7" title="5">{
- s.chatPrefixes = []string{"?", "!", ":", ";"}
- }</span> else<span class="cov5" title="3"> {
- s.chatPrefixes = append([]string{}, opts.ChatPrefixes...)
- }</span>
-
- // Prompts
- <span class="cov10" title="8">s.promptCompSysGeneral = opts.PromptCompSysGeneral
- s.promptCompSysParams = opts.PromptCompSysParams
- s.promptCompSysInline = opts.PromptCompSysInline
- s.promptCompUserGeneral = opts.PromptCompUserGeneral
- s.promptCompUserParams = opts.PromptCompUserParams
- s.promptCompExtraHeader = opts.PromptCompExtraHeader
- s.promptNativeCompletion = opts.PromptNativeCompletion
- s.promptChatSystem = opts.PromptChatSystem
- s.promptRewriteSystem = opts.PromptRewriteSystem
- s.promptDiagnosticsSystem = opts.PromptDiagnosticsSystem
- s.promptDocumentSystem = opts.PromptDocumentSystem
- s.promptRewriteUser = opts.PromptRewriteUser
- s.promptDiagnosticsUser = opts.PromptDiagnosticsUser
- s.promptDocumentUser = opts.PromptDocumentUser
- s.promptGoTestSystem = opts.PromptGoTestSystem
- s.promptGoTestUser = opts.PromptGoTestUser
- s.promptSimplifySystem = opts.PromptSimplifySystem
- s.promptSimplifyUser = opts.PromptSimplifyUser
-
- if len(opts.CustomActions) &gt; 0 </span><span class="cov1" title="1">{
- s.customActions = append([]CustomAction{}, opts.CustomActions...)
- }</span>
-
- <span class="cov10" title="8">if s.inlineOpen != "" </span><span class="cov10" title="8">{
- s.inlineOpenChar = s.inlineOpen[0]
- }</span> else<span class="cov0" title="0"> {
- s.inlineOpenChar = '&gt;'
- }</span>
- <span class="cov10" title="8">if s.inlineClose != "" </span><span class="cov10" title="8">{
- s.inlineCloseChar = s.inlineClose[0]
- }</span> else<span class="cov0" title="0"> {
- s.inlineCloseChar = '&gt;'
- }</span>
- <span class="cov10" title="8">if s.chatSuffix != "" </span><span class="cov10" title="8">{
- s.chatSuffixChar = s.chatSuffix[0]
- }</span> else<span class="cov0" title="0"> {
- s.chatSuffixChar = '&gt;'
- }</span>
+ s.applyOptions(opts)
// Initialize dispatch table
- <span class="cov10" title="8">s.handlers = map[string]func(Request){
+ s.handlers = map[string]func(Request){
"initialize": s.handleInitialize,
"initialized": func(_ Request) </span><span class="cov0" title="0">{ s.handleInitialized() }</span>,
"shutdown": s.handleShutdown,
@@ -6765,7 +6709,221 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions)
"codeAction/resolve": s.handleCodeActionResolve,
"workspace/executeCommand": s.handleExecuteCommand,
}
- <span class="cov10" title="8">return s</span>
+ <span class="cov3" title="7">return s</span>
+}
+
+func (s *Server) applyOptions(opts ServerOptions) <span class="cov4" title="8">{
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.logContext = opts.LogContext
+ if opts.ConfigStore != nil </span><span class="cov1" title="1">{
+ s.configStore = opts.ConfigStore
+ }</span>
+ <span class="cov4" title="8">if opts.Config != nil </span><span class="cov2" title="2">{
+ s.cfg = *opts.Config
+ }</span> else<span class="cov3" title="6"> if opts.ConfigStore != nil </span><span class="cov0" title="0">{
+ s.cfg = opts.ConfigStore.Snapshot()
+ }</span> else<span class="cov3" title="6"> {
+ s.cfg = appconfig.App{}
+ // populate from legacy ServerOptions fields
+ s.cfg.MaxTokens = opts.MaxTokens
+ s.cfg.ContextMode = opts.ContextMode
+ s.cfg.ContextWindowLines = opts.WindowLines
+ s.cfg.MaxContextTokens = opts.MaxContextTokens
+ s.cfg.TriggerCharacters = append([]string{}, opts.TriggerCharacters...)
+ s.cfg.CodingTemperature = opts.CodingTemperature
+ s.cfg.ManualInvokeMinPrefix = opts.ManualInvokeMinPrefix
+ s.cfg.CompletionDebounceMs = opts.CompletionDebounceMs
+ s.cfg.CompletionThrottleMs = opts.CompletionThrottleMs
+ s.cfg.InlineOpen = opts.InlineOpen
+ s.cfg.InlineClose = opts.InlineClose
+ s.cfg.ChatSuffix = opts.ChatSuffix
+ s.cfg.ChatPrefixes = append([]string{}, opts.ChatPrefixes...)
+ s.cfg.PromptCompletionSystemGeneral = opts.PromptCompSysGeneral
+ s.cfg.PromptCompletionSystemParams = opts.PromptCompSysParams
+ s.cfg.PromptCompletionSystemInline = opts.PromptCompSysInline
+ s.cfg.PromptCompletionUserGeneral = opts.PromptCompUserGeneral
+ s.cfg.PromptCompletionUserParams = opts.PromptCompUserParams
+ s.cfg.PromptCompletionExtraHeader = opts.PromptCompExtraHeader
+ s.cfg.PromptNativeCompletion = opts.PromptNativeCompletion
+ s.cfg.PromptChatSystem = opts.PromptChatSystem
+ s.cfg.PromptCodeActionRewriteSystem = opts.PromptRewriteSystem
+ s.cfg.PromptCodeActionDiagnosticsSystem = opts.PromptDiagnosticsSystem
+ s.cfg.PromptCodeActionDocumentSystem = opts.PromptDocumentSystem
+ s.cfg.PromptCodeActionRewriteUser = opts.PromptRewriteUser
+ s.cfg.PromptCodeActionDiagnosticsUser = opts.PromptDiagnosticsUser
+ s.cfg.PromptCodeActionDocumentUser = opts.PromptDocumentUser
+ s.cfg.PromptCodeActionGoTestSystem = opts.PromptGoTestSystem
+ s.cfg.PromptCodeActionGoTestUser = opts.PromptGoTestUser
+ s.cfg.PromptCodeActionSimplifySystem = opts.PromptSimplifySystem
+ s.cfg.PromptCodeActionSimplifyUser = opts.PromptSimplifyUser
+ s.cfg.CustomActions = make([]appconfig.CustomAction, len(opts.CustomActions))
+ for i, ca := range opts.CustomActions </span><span class="cov0" title="0">{
+ s.cfg.CustomActions[i] = appconfig.CustomAction{
+ ID: ca.ID,
+ Title: ca.Title,
+ Kind: ca.Kind,
+ Scope: ca.Scope,
+ Instruction: ca.Instruction,
+ System: ca.System,
+ User: ca.User,
+ }
+ }</span>
+ }
+ <span class="cov4" title="8">s.llmClient = opts.Client</span>
+}
+
+// ApplyOptions updates the server's configuration at runtime.
+func (s *Server) ApplyOptions(opts ServerOptions) <span class="cov1" title="1">{
+ s.applyOptions(opts)
+}</span>
+
+func (s *Server) currentLLMClient() llm.Client <span class="cov8" title="199">{
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ 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">{
+ return s.configStore.Snapshot()
+ }</span>
+ <span class="cov9" title="405">s.mu.RLock()
+ defer s.mu.RUnlock()
+ return s.cfg</span>
+}
+
+func (s *Server) maxTokens() int <span class="cov6" title="35">{
+ cfg := s.currentConfig()
+ if cfg.MaxTokens &lt;= 0 </span><span class="cov6" title="29">{
+ return 500
+ }</span>
+ <span class="cov3" title="6">return cfg.MaxTokens</span>
+}
+
+func (s *Server) contextMode() string <span class="cov4" title="13">{
+ mode := strings.TrimSpace(s.currentConfig().ContextMode)
+ if mode == "" </span><span class="cov3" title="4">{
+ return "file-on-new-func"
+ }</span>
+ <span class="cov4" title="9">return mode</span>
+}
+
+func (s *Server) windowLines() int <span class="cov2" title="2">{
+ cfg := s.currentConfig()
+ if cfg.ContextWindowLines &lt;= 0 </span><span class="cov0" title="0">{
+ return 120
+ }</span>
+ <span class="cov2" title="2">return cfg.ContextWindowLines</span>
+}
+
+func (s *Server) maxContextTokens() int <span class="cov3" title="6">{
+ cfg := s.currentConfig()
+ if cfg.MaxContextTokens &lt;= 0 </span><span class="cov0" title="0">{
+ return 2000
+ }</span>
+ <span class="cov3" title="6">return cfg.MaxContextTokens</span>
+}
+
+func (s *Server) triggerCharacters() []string <span class="cov5" title="27">{
+ cfg := s.currentConfig()
+ if len(cfg.TriggerCharacters) == 0 </span><span class="cov2" title="3">{
+ return []string{".", ":", "/", "_", ")", "{"}
+ }</span>
+ <span class="cov5" title="24">return append([]string{}, cfg.TriggerCharacters...)</span>
+}
+
+func (s *Server) codingTemperature() *float64 <span class="cov6" title="49">{
+ cfg := s.currentConfig()
+ return cfg.CodingTemperature
+}</span>
+
+func (s *Server) manualInvokeMinPrefix() int <span class="cov3" title="5">{
+ return s.currentConfig().ManualInvokeMinPrefix
+}</span>
+
+func (s *Server) completionDebounce() time.Duration <span class="cov6" title="41">{
+ cfg := s.currentConfig()
+ if cfg.CompletionDebounceMs &lt;= 0 </span><span class="cov6" title="39">{
+ return 0
+ }</span>
+ <span class="cov2" title="2">return time.Duration(cfg.CompletionDebounceMs) * time.Millisecond</span>
+}
+
+func (s *Server) completionThrottle() time.Duration <span class="cov6" title="41">{
+ cfg := s.currentConfig()
+ if cfg.CompletionThrottleMs &lt;= 0 </span><span class="cov6" title="38">{
+ return 0
+ }</span>
+ <span class="cov2" title="3">return time.Duration(cfg.CompletionThrottleMs) * time.Millisecond</span>
+}
+
+func (s *Server) inlineMarkers() (open string, close string, openChar byte, closeChar byte) <span class="cov7" title="88">{
+ cfg := s.currentConfig()
+ open = strings.TrimSpace(cfg.InlineOpen)
+ if open == "" </span><span class="cov0" title="0">{
+ open = "&gt;"
+ }</span>
+ <span class="cov7" title="88">close = strings.TrimSpace(cfg.InlineClose)
+ if close == "" </span><span class="cov0" title="0">{
+ close = "&gt;"
+ }</span>
+ <span class="cov7" title="88">openChar = '&gt;'
+ if len(open) &gt; 0 </span><span class="cov7" title="88">{
+ openChar = open[0]
+ }</span>
+ <span class="cov7" title="88">closeChar = '&gt;'
+ if len(close) &gt; 0 </span><span class="cov7" title="88">{
+ closeChar = close[0]
+ }</span>
+ <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">{
+ cfg := s.currentConfig()
+ suffix = cfg.ChatSuffix
+ if suffix != "" </span><span class="cov6" title="42">{
+ suffix = strings.TrimSpace(suffix)
+ if suffix == "" </span><span class="cov0" title="0">{
+ suffix = "&gt;"
+ }</span>
+ } else<span class="cov2" title="2"> {
+ suffix = ""
+ }</span>
+ <span class="cov6" title="44">if len(cfg.ChatPrefixes) == 0 </span><span class="cov0" title="0">{
+ prefixes = []string{"?", "!", ":", ";"}
+ }</span> else<span class="cov6" title="44"> {
+ prefixes = append([]string{}, cfg.ChatPrefixes...)
+ }</span>
+ <span class="cov6" title="44">suffixChar = '&gt;'
+ if len(suffix) &gt; 0 </span><span class="cov6" title="42">{
+ suffixChar = suffix[0]
+ }</span>
+ <span class="cov6" title="44">return suffix, prefixes, suffixChar</span>
+}
+
+func (s *Server) promptSet() appconfig.App <span class="cov2" title="2">{
+ return s.currentConfig()
+}</span>
+
+func (s *Server) customActions() []CustomAction <span class="cov3" title="7">{
+ cfg := s.currentConfig()
+ if len(cfg.CustomActions) == 0 </span><span class="cov1" title="1">{
+ return nil
+ }</span>
+ <span class="cov3" title="6">customs := make([]CustomAction, 0, len(cfg.CustomActions))
+ for _, ca := range cfg.CustomActions </span><span class="cov4" title="10">{
+ customs = append(customs, CustomAction{
+ ID: ca.ID,
+ Title: ca.Title,
+ Kind: ca.Kind,
+ Scope: ca.Scope,
+ Instruction: ca.Instruction,
+ System: ca.System,
+ User: ca.User,
+ })
+ }</span>
+ <span class="cov3" title="6">return customs</span>
}
func (s *Server) Run() error <span class="cov1" title="1">{
@@ -6794,7 +6952,7 @@ func (s *Server) Run() error <span class="cov1" title="1">{
}
</pre>
- <pre class="file" id="file32" style="display: none">// Summary: LSP transport utilities to read and write JSON-RPC messages with Content-Length framing.
+ <pre class="file" id="file33" style="display: none">// Summary: LSP transport utilities to read and write JSON-RPC messages with Content-Length framing.
package lsp
import (
@@ -6865,7 +7023,187 @@ func (s *Server) writeMessage(v any) <span class="cov10" title="41">{
}
</pre>
- <pre class="file" id="file33" style="display: none">//go:build !windows
+ <pre class="file" id="file34" style="display: none">package runtimeconfig
+
+import (
+ "fmt"
+ "log"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+
+ "codeberg.org/snonux/hexai/internal/appconfig"
+)
+
+// Change captures a single configuration delta.
+type Change struct {
+ Key string
+ Old string
+ New string
+}
+
+// Listener receives the previous and new application configuration when updates occur.
+type Listener func(old appconfig.App, new appconfig.App)
+
+// Store holds the active runtime configuration and notifies listeners on updates.
+type Store struct {
+ mu sync.RWMutex
+ cfg appconfig.App
+ listeners map[int]Listener
+ nextID int
+}
+
+// New creates a Store seeded with the provided configuration snapshot.
+func New(cfg appconfig.App) *Store <span class="cov4" title="11">{
+ return &amp;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">{
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ return s.cfg
+}</span>
+
+// Subscribe registers a listener that will be invoked on configuration changes.
+// The returned function removes the listener.
+func (s *Store) Subscribe(listener Listener) func() <span class="cov2" title="2">{
+ if listener == nil </span><span class="cov0" title="0">{
+ return func() </span>{<span class="cov0" title="0">}</span>
+ }
+ <span class="cov2" title="2">s.mu.Lock()
+ id := s.nextID
+ s.nextID++
+ s.listeners[id] = listener
+ s.mu.Unlock()
+ return func() </span><span class="cov0" title="0">{
+ s.mu.Lock()
+ delete(s.listeners, id)
+ s.mu.Unlock()
+ }</span>
+}
+
+// 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">{
+ s.mu.Lock()
+ old := s.cfg
+ s.cfg = cfg
+ listeners := make([]Listener, 0, len(s.listeners))
+ for _, l := range s.listeners </span><span class="cov1" title="1">{
+ listeners = append(listeners, l)
+ }</span>
+ <span class="cov3" title="4">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>
+}
+
+// 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">{
+ 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>
+}
+
+// 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">{
+ 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">{
+ keys[k] = struct{}{}
+ }</span>
+ <span class="cov3" title="4">for k := range after </span><span class="cov7" title="100">{
+ 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">{
+ ordered = append(ordered, k)
+ }</span>
+ <span class="cov3" title="4">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">{
+ 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>
+}
+
+func flattenAppConfig(cfg appconfig.App) map[string]string <span class="cov4" title="8">{
+ result := make(map[string]string)
+ val := reflect.ValueOf(cfg)
+ typ := val.Type()
+ for i := 0; i &lt; typ.NumField(); i++ </span><span class="cov10" title="376">{
+ field := typ.Field(i)
+ key := strings.TrimSpace(field.Tag.Get("toml"))
+ if key == "" || key == "-" </span><span class="cov8" title="184">{
+ switch field.Name </span>{
+ case "StatsWindowMinutes":<span class="cov4" title="8">
+ key = "stats_window_minutes"</span>
+ default:<span class="cov8" title="176">
+ continue</span>
+ }
+ }
+ <span class="cov9" title="200">if idx := strings.Index(key, ","); idx &gt;= 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">{
+ continue</span>
+ }
+ <span class="cov9" title="200">result[key] = stringifyValue(val.Field(i))</span>
+ }
+ <span class="cov4" title="8">return result</span>
+}
+
+func stringifyValue(v reflect.Value) string <span class="cov9" title="224">{
+ 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">
+ return v.String()</span>
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:<span class="cov7" title="64">
+ 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">
+ 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">{
+ return ""
+ }</span>
+ <span class="cov3" title="6">if v.Type().Elem().Kind() == reflect.String </span><span class="cov3" title="6">{
+ parts := make([]string, v.Len())
+ for i := range parts </span><span class="cov5" title="24">{
+ parts[i] = v.Index(i).String()
+ }</span>
+ <span class="cov3" title="6">return strings.Join(parts, ",")</span>
+ }
+ <span class="cov0" title="0">return fmt.Sprint(v.Interface())</span>
+ case reflect.Ptr:<span class="cov6" title="32">
+ if v.IsNil() </span><span class="cov4" title="8">{
+ return "(unset)"
+ }</span>
+ <span class="cov5" title="24">return stringifyValue(v.Elem())</span>
+ default:<span class="cov0" title="0">
+ return fmt.Sprint(v.Interface())</span>
+ }
+}
+</pre>
+
+ <pre class="file" id="file35" style="display: none">//go:build !windows
package stats
@@ -6875,22 +7213,22 @@ import (
"golang.org/x/sys/unix"
)
-func tryLockFile(fd uintptr) error <span class="cov10" title="227">{
- if err := unix.Flock(int(fd), unix.LOCK_EX|unix.LOCK_NB); err != nil </span><span class="cov9" title="153">{
- if errors.Is(err, unix.EWOULDBLOCK) </span><span class="cov9" title="153">{
+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>
}
- <span class="cov8" title="74">return nil</span>
+ <span class="cov8" title="77">return nil</span>
}
-func unlockFile(fd uintptr) error <span class="cov8" title="74">{
+func unlockFile(fd uintptr) error <span class="cov8" title="77">{
return unix.Flock(int(fd), unix.LOCK_UN)
}</span>
</pre>
- <pre class="file" id="file34" style="display: none">// Package stats provides a simple, process-safe, on-disk cache of Hexai LLM usage
+ <pre class="file" id="file36" style="display: none">// Package stats provides a simple, process-safe, on-disk cache of Hexai LLM usage
// statistics shared across all binaries. It appends compact events (ts, provider,
// model, sent, recv) to a JSON file guarded by an advisory file lock, prunes
// entries older than the configured window (default 1h), and computes aggregated
@@ -6921,18 +7259,18 @@ var windowSeconds int64 = int64(defaultWindow.Seconds())
var errLockWouldBlock = errors.New("stats: lock would block")
// SetWindow sets the sliding window used for pruning and aggregation.
-func SetWindow(d time.Duration) <span class="cov5" title="77">{
+func SetWindow(d time.Duration) <span class="cov4" title="82">{
if d &lt; time.Second </span><span class="cov0" title="0">{
d = time.Second
}</span>
- <span class="cov5" title="77">if d &gt; 24*time.Hour </span><span class="cov0" title="0">{
+ <span class="cov4" title="82">if d &gt; 24*time.Hour </span><span class="cov0" title="0">{
d = 24 * time.Hour
}</span>
- <span class="cov5" title="77">atomic.StoreInt64(&amp;windowSeconds, int64(d.Seconds()))</span>
+ <span class="cov4" title="82">atomic.StoreInt64(&amp;windowSeconds, int64(d.Seconds()))</span>
}
// Window returns the current sliding window.
-func Window() time.Duration <span class="cov5" title="74">{ return time.Duration(atomic.LoadInt64(&amp;windowSeconds)) * time.Second }</span>
+func Window() time.Duration <span class="cov4" title="77">{ return time.Duration(atomic.LoadInt64(&amp;windowSeconds)) * time.Second }</span>
// Event represents a single request/response with sizes.
type Event struct {
@@ -6967,108 +7305,108 @@ type Snapshot struct {
}
// Update appends one event and prunes old entries under lock.
-func Update(ctx context.Context, provider, model string, sentBytes, recvBytes int) error <span class="cov5" title="74">{
+func Update(ctx context.Context, provider, model string, sentBytes, recvBytes int) error <span class="cov4" title="77">{
dir, err := CacheDir()
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
- <span class="cov5" title="74">if err := os.MkdirAll(dir, 0o755); err != nil </span><span class="cov0" title="0">{
+ <span class="cov4" title="77">if err := os.MkdirAll(dir, 0o755); err != nil </span><span class="cov0" title="0">{
return err
}</span>
- <span class="cov5" title="74">lockPath := filepath.Join(dir, lockFileName)
+ <span class="cov4" title="77">lockPath := filepath.Join(dir, lockFileName)
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
- <span class="cov5" title="74">defer f.Close()
+ <span class="cov4" title="77">defer f.Close()
unlock, err := acquireFileLock(ctx, f)
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
- <span class="cov5" title="74">defer func() </span><span class="cov5" title="74">{ _ = unlock() }</span>()
+ <span class="cov4" title="77">defer func() </span><span class="cov4" title="77">{ _ = unlock() }</span>()
// Read existing file (if any)
- <span class="cov5" title="74">path := filepath.Join(dir, fileName)
+ <span class="cov4" title="77">path := filepath.Join(dir, fileName)
var sf File
- if b, rerr := os.ReadFile(path); rerr == nil </span><span class="cov5" title="71">{
+ if b, rerr := os.ReadFile(path); rerr == nil </span><span class="cov4" title="74">{
_ = json.Unmarshal(b, &amp;sf)
}</span>
- <span class="cov5" title="74">if sf.Version != fileVersion </span><span class="cov2" title="3">{
+ <span class="cov4" title="77">if sf.Version != fileVersion </span><span class="cov1" title="3">{
sf = File{Version: fileVersion}
}</span>
- <span class="cov5" title="74">now := time.Now()
+ <span class="cov4" title="77">now := time.Now()
win := Window()
sf.WindowSeconds = int(win.Seconds())
// Append event
sf.Events = append(sf.Events, Event{TS: now, Provider: provider, Model: model, Sent: int64(sentBytes), Recv: int64(recvBytes)})
// Prune old
cutoff := now.Add(-win)
- if len(sf.Events) &gt; 0 </span><span class="cov5" title="74">{
+ if len(sf.Events) &gt; 0 </span><span class="cov4" title="77">{
// Find first &gt;= cutoff
i := 0
- for ; i &lt; len(sf.Events); i++ </span><span class="cov5" title="75">{
- if !sf.Events[i].TS.Before(cutoff) </span><span class="cov5" title="74">{
+ for ; i &lt; len(sf.Events); i++ </span><span class="cov4" title="78">{
+ if !sf.Events[i].TS.Before(cutoff) </span><span class="cov4" title="77">{
break</span>
}
}
- <span class="cov5" title="74">if i &gt; 0 </span><span class="cov1" title="1">{
+ <span class="cov4" title="77">if i &gt; 0 </span><span class="cov1" title="1">{
sf.Events = append([]Event(nil), sf.Events[i:]...)
}</span>
}
- <span class="cov5" title="74">sf.UpdatedAt = now
+ <span class="cov4" title="77">sf.UpdatedAt = now
// Write atomically
tmp, err := os.CreateTemp(dir, fileName+".tmp.")
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
- <span class="cov5" title="74">enc := json.NewEncoder(tmp)
+ <span class="cov4" title="77">enc := json.NewEncoder(tmp)
enc.SetEscapeHTML(false)
if err := enc.Encode(&amp;sf); err != nil </span><span class="cov0" title="0">{
tmp.Close()
os.Remove(tmp.Name())
return err
}</span>
- <span class="cov5" title="74">if err := tmp.Sync(); err != nil </span><span class="cov0" title="0">{
+ <span class="cov4" title="77">if err := tmp.Sync(); err != nil </span><span class="cov0" title="0">{
tmp.Close()
os.Remove(tmp.Name())
return err
}</span>
- <span class="cov5" title="74">if err := tmp.Close(); err != nil </span><span class="cov0" title="0">{
+ <span class="cov4" title="77">if err := tmp.Close(); err != nil </span><span class="cov0" title="0">{
os.Remove(tmp.Name())
return err
}</span>
- <span class="cov5" title="74">if err := os.Rename(tmp.Name(), path); err != nil </span><span class="cov0" title="0">{
+ <span class="cov4" title="77">if err := os.Rename(tmp.Name(), path); err != nil </span><span class="cov0" title="0">{
os.Remove(tmp.Name())
return err
}</span>
- <span class="cov5" title="74">return nil</span>
+ <span class="cov4" title="77">return nil</span>
}
-func acquireFileLock(ctx context.Context, f *os.File) (func() error, error) <span class="cov5" title="74">{
+func acquireFileLock(ctx context.Context, f *os.File) (func() error, error) <span class="cov4" title="77">{
fd := f.Fd()
- for </span><span class="cov6" title="227">{
+ for </span><span class="cov5" title="213">{
err := tryLockFile(fd)
- if err == nil </span><span class="cov5" title="74">{
- return func() error </span><span class="cov5" title="74">{ return unlockFile(fd) }</span>, nil
+ if err == nil </span><span class="cov4" title="77">{
+ return func() error </span><span class="cov4" title="77">{ return unlockFile(fd) }</span>, nil
}
- <span class="cov5" title="153">if errors.Is(err, errLockWouldBlock) </span><span class="cov5" title="153">{
+ <span class="cov5" title="136">if errors.Is(err, errLockWouldBlock) </span><span class="cov5" title="136">{
select </span>{
case &lt;-ctx.Done():<span class="cov0" title="0">
return nil, ctx.Err()</span>
- case &lt;-time.After(5 * time.Millisecond):<span class="cov5" title="153"></span>
+ case &lt;-time.After(5 * time.Millisecond):<span class="cov5" title="136"></span>
}
- <span class="cov5" title="153">continue</span>
+ <span class="cov5" title="136">continue</span>
}
<span class="cov0" title="0">return nil, err</span>
}
}
// Snapshot reads and aggregates events within the configured window.
-func TakeSnapshot() (Snapshot, error) <span class="cov5" title="64">{
+func TakeSnapshot() (Snapshot, error) <span class="cov4" title="69">{
dir, err := CacheDir()
if err != nil </span><span class="cov0" title="0">{
return Snapshot{}, err
}</span>
- <span class="cov5" title="64">path := filepath.Join(dir, fileName)
+ <span class="cov4" title="69">path := filepath.Join(dir, fileName)
b, err := os.ReadFile(path)
if err != nil </span><span class="cov0" title="0">{
if errors.Is(err, os.ErrNotExist) </span><span class="cov0" title="0">{
@@ -7076,30 +7414,30 @@ func TakeSnapshot() (Snapshot, error) <span class="cov5" title="64">{
}</span>
<span class="cov0" title="0">return Snapshot{}, err</span>
}
- <span class="cov5" title="64">var sf File
+ <span class="cov4" title="69">var sf File
if err := json.Unmarshal(b, &amp;sf); err != nil </span><span class="cov0" title="0">{
return Snapshot{}, err
}</span>
- <span class="cov5" title="64">win := time.Duration(sf.WindowSeconds) * time.Second
+ <span class="cov4" title="69">win := time.Duration(sf.WindowSeconds) * time.Second
if win &lt;= 0 </span><span class="cov0" title="0">{
win = Window()
- }</span> else<span class="cov5" title="64"> {
+ }</span> else<span class="cov4" title="69"> {
SetWindow(win) // align process with file window if changed elsewhere
}</span>
- <span class="cov5" title="64">cutoff := time.Now().Add(-win)
+ <span class="cov4" 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="11074">{
+ for _, ev := range sf.Events </span><span class="cov10" title="25908">{
if ev.TS.Before(cutoff) </span><span class="cov0" title="0">{
continue</span>
}
- <span class="cov10" title="11074">snap.Global.Reqs++
+ <span class="cov10" title="25908">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="cov6" title="430">{
+ if pe.Models == nil </span><span class="cov6" title="465">{
pe.Models = make(map[string]Counters)
}</span>
- <span class="cov10" title="11074">pe.Totals.Reqs++
+ <span class="cov10" title="25908">pe.Totals.Reqs++
pe.Totals.Sent += ev.Sent
pe.Totals.Recv += ev.Recv
mc := pe.Models[ev.Model]
@@ -7109,37 +7447,37 @@ func TakeSnapshot() (Snapshot, error) <span class="cov5" title="64">{
pe.Models[ev.Model] = mc
snap.Providers[ev.Provider] = pe</span>
}
- <span class="cov5" title="64">mins := win.Minutes()
+ <span class="cov4" title="69">mins := win.Minutes()
if mins &lt;= 0 </span><span class="cov0" title="0">{
mins = 0.001
}</span>
- <span class="cov5" title="64">snap.RPM = float64(snap.Global.Reqs) / mins
+ <span class="cov4" title="69">snap.RPM = float64(snap.Global.Reqs) / mins
return snap, nil</span>
}
// CacheDir resolves the cache directory for stats.
-func CacheDir() (string, error) <span class="cov5" title="139">{
- if x := os.Getenv("XDG_CACHE_HOME"); stringsTrim(x) != "" </span><span class="cov4" title="27">{
+func CacheDir() (string, error) <span class="cov5" title="147">{
+ if x := os.Getenv("XDG_CACHE_HOME"); stringsTrim(x) != "" </span><span class="cov3" title="27">{
return filepath.Join(x, "hexai"), nil
}</span>
- <span class="cov5" title="112">home, err := os.UserHomeDir()
+ <span class="cov5" title="120">home, err := os.UserHomeDir()
if err != nil </span><span class="cov0" title="0">{
return "", fmt.Errorf("cannot resolve home: %w", err)
}</span>
- <span class="cov5" title="112">return filepath.Join(home, ".cache", "hexai"), nil</span>
+ <span class="cov5" title="120">return filepath.Join(home, ".cache", "hexai"), nil</span>
}
// stringsTrim is a tiny helper to avoid importing strings everywhere here.
-func stringsTrim(s string) string <span class="cov5" title="139">{
+func stringsTrim(s string) string <span class="cov5" title="147">{
i := 0
j := len(s)
for i &lt; j &amp;&amp; (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') </span><span class="cov0" title="0">{
i++
}</span>
- <span class="cov5" title="139">for j &gt; i &amp;&amp; (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') </span><span class="cov0" title="0">{
+ <span class="cov5" title="147">for j &gt; i &amp;&amp; (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') </span><span class="cov0" title="0">{
j--
}</span>
- <span class="cov5" title="139">if i == 0 &amp;&amp; j == len(s) </span><span class="cov5" title="139">{
+ <span class="cov5" title="147">if i == 0 &amp;&amp; j == len(s) </span><span class="cov5" title="147">{
return s
}</span>
<span class="cov0" title="0">return s[i:j]</span>
@@ -7151,7 +7489,7 @@ func (s Snapshot) DebugString() string <span class="cov1" title="1">{
}</span>
</pre>
- <pre class="file" id="file35" style="display: none">package testutil
+ <pre class="file" id="file37" style="display: none">package testutil
// MultilineDocBlock returns a realistic multi-line documentation block.
func MultilineDocBlock() string <span class="cov8" title="1">{
@@ -7179,83 +7517,83 @@ func MalformedJSON() string <span class="cov8" title="1">{
}</span>
</pre>
- <pre class="file" id="file36" style="display: none">package textutil
+ <pre class="file" id="file38" style="display: none">package textutil
import "fmt"
// HumanBytes renders n in a short human-friendly form using base-1000 units.
// Examples: 999 -&gt; 999B, 1200 -&gt; 1.2k, 1540000 -&gt; 1.5M
-func HumanBytes(n int64) string <span class="cov10" title="128">{
+func HumanBytes(n int64) string <span class="cov10" title="138">{
if n &lt; 1000 </span><span class="cov2" title="2">{
return fmt.Sprintf("%dB", n)
}</span>
- <span class="cov9" title="126">const unit = 1000.0
+ <span class="cov9" title="136">const unit = 1000.0
v := float64(n)
suffix := []string{"k", "M", "G", "T"}
i := 0
- for v &gt;= unit &amp;&amp; i &lt; len(suffix)-1 </span><span class="cov9" title="126">{
+ for v &gt;= unit &amp;&amp; i &lt; len(suffix)-1 </span><span class="cov9" title="136">{
v /= unit
i++
}</span>
- <span class="cov9" title="126">s := fmt.Sprintf("%.1f%s", v, suffix[i])
+ <span class="cov9" title="136">s := fmt.Sprintf("%.1f%s", v, suffix[i])
// Strip trailing ".0"
if len(s) &gt;= 3 &amp;&amp; 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="126">return s</span>
+ <span class="cov9" title="136">return s</span>
}
</pre>
- <pre class="file" id="file37" style="display: none">package textutil
+ <pre class="file" id="file39" style="display: none">package textutil
import "strings"
// RenderTemplate performs simple {{var}} replacement in a template string.
-func RenderTemplate(t string, vars map[string]string) string <span class="cov8" title="58">{
- if t == "" || len(vars) == 0 </span><span class="cov5" title="11">{
+func RenderTemplate(t string, vars map[string]string) string <span class="cov8" title="63">{
+ if t == "" || len(vars) == 0 </span><span class="cov3" title="5">{
return t
}</span>
- <span class="cov8" title="47">out := t
- for k, v := range vars </span><span class="cov10" title="115">{
+ <span class="cov8" title="58">out := t
+ for k, v := range vars </span><span class="cov10" title="156">{
out = strings.ReplaceAll(out, "{{"+k+"}}", v)
}</span>
- <span class="cov8" title="47">return out</span>
+ <span class="cov8" title="58">return out</span>
}
// StripCodeFences removes surrounding Markdown triple-backtick fences.
-func StripCodeFences(s string) string <span class="cov8" title="65">{
+func StripCodeFences(s string) string <span class="cov8" title="69">{
t := strings.TrimSpace(s)
if t == "" </span><span class="cov1" title="1">{
return t
}</span>
- <span class="cov8" title="64">lines := strings.Split(t, "\n")
+ <span class="cov8" title="68">lines := strings.Split(t, "\n")
start := 0
for start &lt; len(lines) &amp;&amp; strings.TrimSpace(lines[start]) == "" </span><span class="cov0" title="0">{
start++
}</span>
- <span class="cov8" title="64">end := len(lines) - 1
+ <span class="cov8" title="68">end := len(lines) - 1
for end &gt;= 0 &amp;&amp; strings.TrimSpace(lines[end]) == "" </span><span class="cov0" title="0">{
end--
}</span>
- <span class="cov8" title="64">if start &gt;= len(lines) || end &lt; 0 || start &gt; end </span><span class="cov0" title="0">{
+ <span class="cov8" title="68">if start &gt;= len(lines) || end &lt; 0 || start &gt; end </span><span class="cov0" title="0">{
return t
}</span>
- <span class="cov8" title="64">first := strings.TrimSpace(lines[start])
+ <span class="cov8" title="68">first := strings.TrimSpace(lines[start])
last := strings.TrimSpace(lines[end])
if strings.HasPrefix(first, "```") &amp;&amp; last == "```" &amp;&amp; end &gt; start </span><span class="cov6" title="20">{
inner := strings.Join(lines[start+1:end], "\n")
return inner
}</span>
- <span class="cov8" title="44">return t</span>
+ <span class="cov7" title="48">return t</span>
}
// InstructionFromSelection extracts the first inline instruction and returns
// (instruction, cleanedSelection). It detects markers on the earliest position
// per line in precedence: strict ;text;, /* */, &lt;!-- --&gt;, //, #, --.
-func InstructionFromSelection(sel string) (string, string) <span class="cov6" title="14">{
+func InstructionFromSelection(sel string) (string, string) <span class="cov5" title="14">{
lines := strings.Split(sel, "\n")
- for idx, line := range lines </span><span class="cov6" title="14">{
- if instr, cleaned, ok := FindFirstInstructionInLine(line); ok &amp;&amp; strings.TrimSpace(instr) != "" </span><span class="cov6" title="14">{
+ for idx, line := range lines </span><span class="cov5" title="14">{
+ if instr, cleaned, ok := FindFirstInstructionInLine(line); ok &amp;&amp; strings.TrimSpace(instr) != "" </span><span class="cov5" title="14">{
lines[idx] = cleaned
return instr, strings.Join(lines, "\n")
}</span>
@@ -7264,16 +7602,16 @@ func InstructionFromSelection(sel string) (string, string) <span class="cov6" ti
}
// FindFirstInstructionInLine returns (instruction, cleaned, ok) for a single line.
-func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <span class="cov6" title="15">{
+func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <span class="cov5" title="15">{
type cand struct {
start, end int
text string
}
cands := []cand{}
- if t, l, r, ok := FindStrictInlineTag(line); ok </span><span class="cov4" title="5">{
+ if t, l, r, ok := FindStrictInlineTag(line); ok </span><span class="cov3" title="5">{
cands = append(cands, cand{start: l, end: r, text: t})
}</span>
- <span class="cov6" title="15">if i := strings.Index(line, "/*"); i &gt;= 0 </span><span class="cov2" title="2">{
+ <span class="cov5" title="15">if i := strings.Index(line, "/*"); i &gt;= 0 </span><span class="cov2" title="2">{
if j := strings.Index(line[i+2:], "*/"); j &gt;= 0 </span><span class="cov2" title="2">{
start := i
end := i + 2 + j + 2
@@ -7281,7 +7619,7 @@ func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <s
cands = append(cands, cand{start: start, end: end, text: text})
}</span>
}
- <span class="cov6" title="15">if i := strings.Index(line, "&lt;!--"); i &gt;= 0 </span><span class="cov2" title="2">{
+ <span class="cov5" title="15">if i := strings.Index(line, "&lt;!--"); i &gt;= 0 </span><span class="cov2" title="2">{
if j := strings.Index(line[i+4:], "--&gt;"); j &gt;= 0 </span><span class="cov2" title="2">{
start := i
end := i + 4 + j + 3
@@ -7289,25 +7627,25 @@ func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <s
cands = append(cands, cand{start: start, end: end, text: text})
}</span>
}
- <span class="cov6" title="15">if i := strings.Index(line, "//"); i &gt;= 0 </span><span class="cov3" title="3">{
+ <span class="cov5" title="15">if i := strings.Index(line, "//"); i &gt;= 0 </span><span class="cov2" title="3">{
cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])})
}</span>
- <span class="cov6" title="15">if i := strings.Index(line, "#"); i &gt;= 0 </span><span class="cov2" title="2">{
+ <span class="cov5" title="15">if i := strings.Index(line, "#"); i &gt;= 0 </span><span class="cov2" title="2">{
cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+1:])})
}</span>
- <span class="cov6" title="15">if i := strings.Index(line, "--"); i &gt;= 0 </span><span class="cov3" title="4">{
+ <span class="cov5" title="15">if i := strings.Index(line, "--"); i &gt;= 0 </span><span class="cov3" title="4">{
cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])})
}</span>
- <span class="cov6" title="15">if len(cands) == 0 </span><span class="cov0" title="0">{
+ <span class="cov5" title="15">if len(cands) == 0 </span><span class="cov0" title="0">{
return "", line, false
}</span>
- <span class="cov6" title="15">best := cands[0]
- for _, c := range cands[1:] </span><span class="cov3" title="3">{
+ <span class="cov5" title="15">best := cands[0]
+ for _, c := range cands[1:] </span><span class="cov2" title="3">{
if c.start &gt;= 0 &amp;&amp; (best.start &lt; 0 || c.start &lt; best.start) </span><span class="cov0" title="0">{
best = c
}</span>
}
- <span class="cov6" title="15">cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t")
+ <span class="cov5" title="15">cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t")
return best.text, cleaned, true</span>
}
@@ -7320,7 +7658,7 @@ func FindStrictInlineTag(line string) (text string, left, right int, ok bool) <s
<span class="cov4" title="8">if i+1 &lt; len(line) &amp;&amp; line[i+1] == ' ' </span><span class="cov1" title="1">{
continue</span>
}
- <span class="cov4" title="7">for j := i + 1; j &lt; len(line); j++ </span><span class="cov8" title="41">{
+ <span class="cov4" title="7">for j := i + 1; j &lt; len(line); j++ </span><span class="cov7" title="41">{
if line[j] == ';' </span><span class="cov4" title="6">{
if j-1 &gt;= 0 &amp;&amp; line[j-1] == ' ' </span><span class="cov0" title="0">{
continue</span>
@@ -7336,7 +7674,7 @@ func FindStrictInlineTag(line string) (text string, left, right int, ok bool) <s
}
</pre>
- <pre class="file" id="file38" style="display: none">package tmux
+ <pre class="file" id="file40" style="display: none">package tmux
import (
"fmt"
@@ -7360,30 +7698,30 @@ const (
)
// Enabled reports whether tmux status updates are enabled via env (default: on).
-func Enabled() bool <span class="cov7" title="72">{
+func Enabled() bool <span class="cov8" title="77">{
v := strings.TrimSpace(os.Getenv("HEXAI_TMUX_STATUS"))
- if v == "" </span><span class="cov7" title="72">{
+ if v == "" </span><span class="cov7" title="74">{
return true
}</span>
- <span class="cov0" title="0">v = strings.ToLower(v)
+ <span class="cov2" title="3">v = strings.ToLower(v)
return v == "1" || v == "true" || v == "yes" || v == "on"</span>
}
// SetUserOption sets a global tmux user option like @hexai_status to value.
-func SetUserOption(key, value string) error <span class="cov7" title="72">{
- if !Enabled() || !HasBinary() || !InSession() </span><span class="cov0" title="0">{
+func SetUserOption(key, value string) error <span class="cov8" title="77">{
+ if !Enabled() || !HasBinary() || !InSession() </span><span class="cov2" title="3">{
return nil
}</span>
- <span class="cov7" title="72">k := strings.TrimPrefix(strings.TrimSpace(key), "@")
+ <span class="cov7" title="74">k := strings.TrimPrefix(strings.TrimSpace(key), "@")
if k == "" </span><span class="cov0" title="0">{
return nil
}</span>
// Use set-option -g so it appears for all windows
- <span class="cov7" title="72">return exec.Command("tmux", "set-option", "-g", "@"+k, value).Run()</span>
+ <span class="cov7" title="74">return exec.Command("tmux", "set-option", "-g", "@"+k, value).Run()</span>
}
// SetStatus is a convenience for setting @hexai_status.
-func SetStatus(value string) error <span class="cov7" title="72">{ return SetUserOption("hexai_status", applyTheme(value)) }</span>
+func SetStatus(value string) error <span class="cov8" title="77">{ return SetUserOption("hexai_status", applyTheme(value)) }</span>
// FormatLLMStatsStatus builds a compact tmux status string for LLM heartbeats.
// Example: "LLM:gpt-4.1 5r 0.8rpm in12k out34k"
@@ -7409,7 +7747,7 @@ func FormatLLMStatsStatusColored(provider, model string, reqs int64, rpm float64
// scoped provider:model tail. The window indicator (e.g., Σ@1h) should be composed
// by the caller if needed; this function focuses on numbers and labels.
// Example: "Σ ↑120k ↓340k 4.2rpm | openai:gpt-4.1 3.1rpm 80r"
-func FormatGlobalStatusColored(globalReqs int64, globalRPM float64, globalIn, globalOut int64, scopeProvider, scopeModel string, scopeRPM float64, scopeReqs int64, window time.Duration) string <span class="cov7" title="62">{
+func FormatGlobalStatusColored(globalReqs int64, globalRPM float64, globalIn, globalOut int64, scopeProvider, scopeModel string, scopeRPM float64, scopeReqs int64, window time.Duration) string <span class="cov7" title="67">{
gin := textutil.HumanBytes(globalIn)
gout := textutil.HumanBytes(globalOut)
head := fmt.Sprintf("%sΣ@%s %s↑%s%s %s↓%s%s %.1frpm", baseFGToken, humanWindow(window), arrowUpToken, baseFGToken, gin, arrowDownToken, baseFGToken, gout, globalRPM)
@@ -7417,7 +7755,7 @@ func FormatGlobalStatusColored(globalReqs int64, globalRPM float64, globalIn, gl
if narrowEnabled() || stringsTrim(scopeProvider) == "" || stringsTrim(scopeModel) == "" </span><span class="cov1" title="1">{
return head
}</span>
- <span class="cov7" title="61">tail := fmt.Sprintf(" | %s:%s %.1frpm %dr", scopeProvider, scopeModel, scopeRPM, scopeReqs)
+ <span class="cov7" title="66">tail := fmt.Sprintf(" | %s:%s %.1frpm %dr", scopeProvider, scopeModel, scopeRPM, scopeReqs)
// Respect max length when configured: drop tail if it would overflow
if ml := maxStatusLen(); ml &gt; 0 </span><span class="cov1" title="1">{
if len(head) &lt;= ml &amp;&amp; len(head)+len(tail) &gt; ml </span><span class="cov0" title="0">{
@@ -7427,15 +7765,15 @@ func FormatGlobalStatusColored(globalReqs int64, globalRPM float64, globalIn, gl
return truncateStatus(head, ml)
}</span>
}
- <span class="cov7" title="60">return head + tail</span>
+ <span class="cov7" title="65">return head + tail</span>
}
-func humanWindow(d time.Duration) string <span class="cov7" title="62">{
+func humanWindow(d time.Duration) string <span class="cov7" title="67">{
if d &lt;= 0 </span><span class="cov0" title="0">{
return "?"
}</span>
- <span class="cov7" title="62">mins := int(d.Minutes())
- if mins%60 == 0 </span><span class="cov7" title="60">{
+ <span class="cov7" title="67">mins := int(d.Minutes())
+ if mins%60 == 0 </span><span class="cov7" title="65">{
return fmt.Sprintf("%dh", mins/60)
}</span>
<span class="cov2" title="2">if mins &gt;= 60 </span><span class="cov0" title="0">{
@@ -7445,9 +7783,9 @@ func humanWindow(d time.Duration) string <span class="cov7" title="62">{
}
// narrowEnabled returns true when HEXAI_TMUX_STATUS_NARROW is truthy (1/true/yes/on).
-func narrowEnabled() bool <span class="cov7" title="62">{
+func narrowEnabled() bool <span class="cov7" title="67">{
v := strings.ToLower(stringsTrim(os.Getenv("HEXAI_TMUX_STATUS_NARROW")))
- if v == "" </span><span class="cov7" title="61">{
+ if v == "" </span><span class="cov7" title="66">{
return false
}</span>
<span class="cov1" title="1">switch v </span>{
@@ -7459,9 +7797,9 @@ func narrowEnabled() bool <span class="cov7" title="62">{
}
// maxStatusLen returns HEXAI_TMUX_STATUS_MAXLEN parsed as int; 0 disables.
-func maxStatusLen() int <span class="cov7" title="61">{
+func maxStatusLen() int <span class="cov7" title="66">{
v := stringsTrim(os.Getenv("HEXAI_TMUX_STATUS_MAXLEN"))
- if v == "" </span><span class="cov7" title="60">{
+ if v == "" </span><span class="cov7" title="65">{
return 0
}</span>
<span class="cov1" title="1">n, err := strconv.Atoi(v)
@@ -7484,16 +7822,16 @@ func truncateStatus(s string, n int) string <span class="cov1" title="1">{
<span class="cov1" title="1">return s[:n-1] + "…"</span>
}
-func stringsTrim(s string) string <span class="cov10" title="245">{
+func stringsTrim(s string) string <span class="cov10" title="265">{
i := 0
j := len(s)
for i &lt; j &amp;&amp; (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') </span><span class="cov0" title="0">{
i++
}</span>
- <span class="cov10" title="245">for j &gt; i &amp;&amp; (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') </span><span class="cov0" title="0">{
+ <span class="cov10" title="265">for j &gt; i &amp;&amp; (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') </span><span class="cov0" title="0">{
j--
}</span>
- <span class="cov10" title="245">if i == 0 &amp;&amp; j == len(s) </span><span class="cov10" title="245">{
+ <span class="cov10" title="265">if i == 0 &amp;&amp; j == len(s) </span><span class="cov10" title="265">{
return s
}</span>
<span class="cov0" title="0">return s[i:j]</span>
@@ -7507,7 +7845,7 @@ func FormatLLMStartStatus(provider, model string) string <span class="cov5" titl
// applyTheme wraps the status string with a user-selected tmux style if requested.
// Set HEXAI_TMUX_STATUS_THEME=white-on-purple to get white-on-purple background.
-func applyTheme(s string) string <span class="cov7" title="72">{
+func applyTheme(s string) string <span class="cov8" title="77">{
theme := strings.ToLower(strings.TrimSpace(os.Getenv("HEXAI_TMUX_STATUS_THEME")))
// Allow explicit fg/bg override
fg := strings.TrimSpace(os.Getenv("HEXAI_TMUX_STATUS_FG"))
@@ -7523,23 +7861,23 @@ func applyTheme(s string) string <span class="cov7" title="72">{
baseFG = fg
}</span>
// bg used as provided (may be empty)
- } else<span class="cov7" title="72"> {
+ } else<span class="cov8" title="77"> {
switch theme </span>{
- case "white-on-purple", "purple", "magenta", "white-on-magenta":<span class="cov7" title="72">
+ case "white-on-purple", "purple", "magenta", "white-on-magenta":<span class="cov8" title="77">
baseFG, bg, wrap = "white", "magenta", true</span>
case "black-on-yellow", "yellow", "black-on-gold":<span class="cov0" title="0">
baseFG, bg, wrap = "black", "yellow", true</span>
case "white-on-blue", "blue", "white-on-navy":<span class="cov0" title="0">
baseFG, bg, wrap = "white", "blue", true</span>
}
- <span class="cov7" title="72">if baseFG == "" </span><span class="cov0" title="0">{ // no theme selected
+ <span class="cov8" title="77">if baseFG == "" </span><span class="cov0" title="0">{ // no theme selected
baseFG = "default"
}</span>
}
// Theme-aware arrow styles
- <span class="cov7" title="72">upStyle, downStyle := "#[fg=colour3]", "#[fg=colour2]" // defaults: yellow up, green down
- if fg != "" || bg != "" </span><span class="cov7" title="72">{ // explicit override path: match arrows to base fg, bold for visibility
+ <span class="cov8" title="77">upStyle, downStyle := "#[fg=colour3]", "#[fg=colour2]" // defaults: yellow up, green down
+ if fg != "" || bg != "" </span><span class="cov8" title="77">{ // explicit override path: match arrows to base fg, bold for visibility
upStyle = "#[bold,fg=" + baseFG + "]"
downStyle = upStyle
}</span> else<span class="cov0" title="0"> {
@@ -7554,30 +7892,30 @@ func applyTheme(s string) string <span class="cov7" title="72">{
}
// Replace base-foreground and arrow placeholders with selected styles
- <span class="cov7" title="72">if strings.Contains(s, baseFGToken) </span><span class="cov7" title="72">{
+ <span class="cov8" title="77">if strings.Contains(s, baseFGToken) </span><span class="cov8" title="77">{
s = strings.ReplaceAll(s, baseFGToken, "#[fg="+baseFG+"]")
}</span>
- <span class="cov7" title="72">if strings.Contains(s, arrowUpToken) </span><span class="cov7" title="60">{
+ <span class="cov8" title="77">if strings.Contains(s, arrowUpToken) </span><span class="cov7" title="65">{
s = strings.ReplaceAll(s, arrowUpToken, upStyle)
}</span>
- <span class="cov7" title="72">if strings.Contains(s, arrowDownToken) </span><span class="cov7" title="60">{
+ <span class="cov8" title="77">if strings.Contains(s, arrowDownToken) </span><span class="cov7" title="65">{
s = strings.ReplaceAll(s, arrowDownToken, downStyle)
}</span>
- <span class="cov7" title="72">if !wrap </span><span class="cov0" title="0">{
+ <span class="cov8" title="77">if !wrap </span><span class="cov0" title="0">{
return s
}</span>
// Wrap with base fg and optional bg, then reset at the end
- <span class="cov7" title="72">prefix := "#[fg=" + baseFG
- if bg != "" </span><span class="cov7" title="72">{
+ <span class="cov8" title="77">prefix := "#[fg=" + baseFG
+ if bg != "" </span><span class="cov8" title="77">{
prefix += ",bg=" + bg
}</span>
- <span class="cov7" title="72">prefix += "]"
+ <span class="cov8" title="77">prefix += "]"
return prefix + s + "#[fg=default,bg=default]"</span>
}
</pre>
- <pre class="file" id="file39" style="display: none">package tmux
+ <pre class="file" id="file41" style="display: none">package tmux
import (
"os"
@@ -7595,10 +7933,10 @@ var (
command = exec.Command
)
-func HasBinary() bool <span class="cov10" title="76">{ _, err := lookPath("tmux"); return err == nil }</span>
+func HasBinary() bool <span class="cov10" title="78">{ _, err := lookPath("tmux"); return err == nil }</span>
// InSession reports whether we seem to be running inside a tmux session.
-func InSession() bool <span class="cov9" title="75">{ return strings.TrimSpace(os.Getenv("TMUX")) != "" }</span>
+func InSession() bool <span class="cov9" title="77">{ return strings.TrimSpace(os.Getenv("TMUX")) != "" }</span>
// SplitOpts controls how a new pane is created for running a command.
type SplitOpts struct {