summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-19 22:52:48 +0300
committerPaul Buetow <paul@buetow.org>2025-09-19 22:52:48 +0300
commiteb72b06fe8e62cb77af73f6dc558d384a5a5fe80 (patch)
treeefeb1165b9fbcb69a4ee675dba7bdc8c28fee3aa
parentacc400768153a7bfda1413f15579c9455b877c87 (diff)
fix
-rw-r--r--SCRATCHPAD.md7
-rw-r--r--docs/coverage.html1347
-rw-r--r--docs/coverage.out34217
-rw-r--r--internal/appconfig/config.go82
-rw-r--r--internal/hexaiaction/run.go109
-rw-r--r--internal/hexaicli/run.go8
-rw-r--r--internal/hexaicli/run_test.go13
-rw-r--r--internal/llm/ollama.go2
-rw-r--r--internal/lsp/chat_history_test.go11
-rw-r--r--internal/lsp/chat_no_double_answer_test.go1
-rw-r--r--internal/lsp/chat_trigger_suppression_test.go1
-rw-r--r--internal/lsp/codeaction_custom_test.go13
-rw-r--r--internal/lsp/codeaction_gotest_int_test.go1
-rw-r--r--internal/lsp/completion_codex_path_test.go2
-rw-r--r--internal/lsp/completion_prefix_strip_test.go34
-rw-r--r--internal/lsp/coverage_add_test.go12
-rw-r--r--internal/lsp/diagnostics_action_test.go1
-rw-r--r--internal/lsp/document_handlers_test.go3
-rw-r--r--internal/lsp/document_test.go44
-rw-r--r--internal/lsp/handlers.go12
-rw-r--r--internal/lsp/handlers_codeaction.go2
-rw-r--r--internal/lsp/handlers_completion.go98
-rw-r--r--internal/lsp/handlers_document.go29
-rw-r--r--internal/lsp/handlers_end_to_end_test.go5
-rw-r--r--internal/lsp/handlers_helpers_test.go6
-rw-r--r--internal/lsp/handlers_test.go41
-rw-r--r--internal/lsp/handlers_utils.go52
-rw-r--r--internal/lsp/helpers_inline_prompt_test.go4
-rw-r--r--internal/lsp/helpers_more_test.go12
-rw-r--r--internal/lsp/init_and_trigger_test.go1
-rw-r--r--internal/lsp/init_shutdown_test.go1
-rw-r--r--internal/lsp/instruction_table_test.go3
-rw-r--r--internal/lsp/server.go28
-rw-r--r--internal/lsp/transport.go3
-rw-r--r--internal/lsp/triggers_config_test.go12
-rw-r--r--internal/stats/stats.go38
36 files changed, 18934 insertions, 17321 deletions
diff --git a/SCRATCHPAD.md b/SCRATCHPAD.md
index d9874bc..1e5fb80 100644
--- a/SCRATCHPAD.md
+++ b/SCRATCHPAD.md
@@ -4,12 +4,7 @@ This document shows future items and items in progress. Already completed ones a
## Features
-* [/] Keep global stats about LLM usage for the tmux pane!
-* [/] No a feature, but verify my OpenAI API account so I can use GPT-5 via the API.
- * [ ] Temperature must by default be 1 for GPT-5
- * [ ] Answers aren't streamed ot th eCLI anymore?
- * [ ] GPT-5 is timing out on large responses?
- * [ ] Any more tweaks for GPT-5 API?
+* [ ] Fix that ollama fallback model as there is an extra backtick in it
* [ ] Kagi FastGPT for in-editor search
- Think about an in-editor chat trigger, maybe with S> for search!
* [ ] Test whethe GitHub Copilot support actually works now, and if not, fix it!
diff --git a/docs/coverage.html b/docs/coverage.html
index 4c7532e..6828b9a 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 (90.6%)</option>
+ <option value="file3">codeberg.org/snonux/hexai/internal/appconfig/config.go (88.8%)</option>
<option value="file4">codeberg.org/snonux/hexai/internal/editor/editor.go (58.3%)</option>
@@ -69,9 +69,9 @@
<option value="file6">codeberg.org/snonux/hexai/internal/hexaiaction/parse.go (92.6%)</option>
- <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (92.7%)</option>
+ <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 (69.7%)</option>
+ <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (71.0%)</option>
<option value="file9">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (65.5%)</option>
@@ -79,7 +79,7 @@
<option value="file11">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (100.0%)</option>
- <option value="file12">codeberg.org/snonux/hexai/internal/hexaicli/run.go (89.7%)</option>
+ <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>
@@ -87,7 +87,7 @@
<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 (85.5%)</option>
+ <option value="file16">codeberg.org/snonux/hexai/internal/llm/openai.go (87.1%)</option>
<option value="file17">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option>
@@ -107,7 +107,7 @@
<option value="file25">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (82.3%)</option>
- <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (88.0%)</option>
+ <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.2%)</option>
<option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (90.1%)</option>
@@ -115,23 +115,25 @@
<option value="file29">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (63.6%)</option>
- <option value="file30">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (89.5%)</option>
+ <option value="file30">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (90.0%)</option>
- <option value="file31">codeberg.org/snonux/hexai/internal/lsp/server.go (83.0%)</option>
+ <option value="file31">codeberg.org/snonux/hexai/internal/lsp/server.go (79.8%)</option>
- <option value="file32">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option>
+ <option value="file32">codeberg.org/snonux/hexai/internal/lsp/transport.go (73.0%)</option>
- <option value="file33">codeberg.org/snonux/hexai/internal/stats/stats.go (75.4%)</option>
+ <option value="file33">codeberg.org/snonux/hexai/internal/stats/lock_posix.go (83.3%)</option>
- <option value="file34">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option>
+ <option value="file34">codeberg.org/snonux/hexai/internal/stats/stats.go (75.8%)</option>
- <option value="file35">codeberg.org/snonux/hexai/internal/textutil/human.go (92.3%)</option>
+ <option value="file35">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option>
- <option value="file36">codeberg.org/snonux/hexai/internal/textutil/textutil.go (90.4%)</option>
+ <option value="file36">codeberg.org/snonux/hexai/internal/textutil/human.go (92.3%)</option>
- <option value="file37">codeberg.org/snonux/hexai/internal/tmux/status.go (73.8%)</option>
+ <option value="file37">codeberg.org/snonux/hexai/internal/textutil/textutil.go (90.4%)</option>
- <option value="file38">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option>
+ <option value="file38">codeberg.org/snonux/hexai/internal/tmux/status.go (73.8%)</option>
+
+ <option value="file39">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option>
</select>
</div>
@@ -347,7 +349,7 @@ type CustomAction struct {
}
// Constructor: defaults for App (kept first among functions)
-func newDefaultConfig() App <span class="cov5" title="31">{
+func newDefaultConfig() App <span class="cov5" title="37">{
// Coding-friendly default temperature across providers
// Users can override per provider in config.toml (including 0.0).
t := 0.2
@@ -403,18 +405,18 @@ func newDefaultConfig() App <span class="cov5" title="31">{
// 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="30">{
+func Load(logger *log.Logger) App <span class="cov5" title="36">{
cfg := newDefaultConfig()
if logger == nil </span><span class="cov4" title="9">{
return cfg // Return defaults if no logger is provided (e.g. in tests)
}</span>
- <span class="cov5" title="21">configPath, err := getConfigPath()
+ <span class="cov5" title="27">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="21"> {
- if fileCfg, err := loadFromFile(configPath, logger); err == nil &amp;&amp; fileCfg != nil </span><span class="cov5" title="16">{
+ }</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">{
cfg.mergeWith(fileCfg)
}</span>
// When the config file is missing or invalid, we keep defaults and still
@@ -422,10 +424,10 @@ func Load(logger *log.Logger) App <span class="cov5" title="30">{
}
// Environment overrides (take precedence over file)
- <span class="cov5" title="21">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov4" title="12">{
+ <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="21">return cfg</span>
+ <span class="cov5" title="27">return cfg</span>
}
// Private helpers
@@ -488,9 +490,36 @@ type sectionStats struct {
}
type sectionOpenAI struct {
- Model string `toml:"model"`
- BaseURL string `toml:"base_url"`
- Temperature *float64 `toml:"temperature"`
+ Model string `toml:"model"`
+ BaseURL string `toml:"base_url"`
+ Temperature *float64 `toml:"temperature"`
+ Presets map[string]string `toml:"presets"`
+}
+
+func (s sectionOpenAI) isZero() bool <span class="cov5" title="22">{
+ return strings.TrimSpace(s.Model) == "" &amp;&amp; strings.TrimSpace(s.BaseURL) == "" &amp;&amp; s.Temperature == nil &amp;&amp; len(s.Presets) == 0
+}</span>
+
+func (s sectionOpenAI) resolvedModel() string <span class="cov2" title="4">{
+ model := strings.TrimSpace(s.Model)
+ if model == "" </span><span class="cov0" title="0">{
+ return ""
+ }</span>
+ <span class="cov2" title="4">if len(s.Presets) == 0 </span><span class="cov2" title="3">{
+ return model
+ }</span>
+ <span class="cov1" title="1">if mapped := strings.TrimSpace(s.Presets[model]); mapped != "" </span><span class="cov1" title="1">{
+ return mapped
+ }</span>
+ <span class="cov0" title="0">lower := strings.ToLower(model)
+ for k, v := range s.Presets </span><span class="cov0" title="0">{
+ if strings.ToLower(strings.TrimSpace(k)) == lower </span><span class="cov0" title="0">{
+ if mapped := strings.TrimSpace(v); mapped != "" </span><span class="cov0" title="0">{
+ return mapped
+ }</span>
+ }
+ }
+ <span class="cov0" title="0">return model</span>
}
type sectionCopilot struct {
@@ -565,7 +594,7 @@ type sectionTmux struct {
CustomMenuHotkey string `toml:"custom_menu_hotkey"`
}
-func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
+func (fc *fileConfig) toApp() App <span class="cov5" title="22">{
out := App{}
// Merge section: general
@@ -581,13 +610,13 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
}</span>
// logging
- <span class="cov5" title="16">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{
+ <span class="cov5" title="22">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="16">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{
+ <span class="cov5" title="22">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{
tmp := App{
CompletionDebounceMs: fc.Completion.CompletionDebounceMs,
CompletionThrottleMs: fc.Completion.CompletionThrottleMs,
@@ -597,41 +626,41 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
}</span>
// triggers
- <span class="cov5" title="16">if len(fc.Triggers.TriggerCharacters) &gt; 0 </span><span class="cov2" title="3">{
+ <span class="cov5" title="22">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="16">if (fc.Inline != sectionInline{}) </span><span class="cov1" title="1">{
+ <span class="cov5" title="22">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="16">if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) &gt; 0 </span><span class="cov1" title="1">{
+ <span class="cov5" title="22">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="16">if strings.TrimSpace(fc.Provider.Name) != "" </span><span class="cov2" title="3">{
+ <span class="cov5" title="22">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="16">if (fc.OpenAI != sectionOpenAI{}) || fc.OpenAI.Temperature != nil </span><span class="cov2" title="3">{
+ <span class="cov5" title="22">if !fc.OpenAI.isZero() || fc.OpenAI.Temperature != nil </span><span class="cov2" title="4">{
tmp := App{
OpenAIBaseURL: fc.OpenAI.BaseURL,
- OpenAIModel: fc.OpenAI.Model,
+ OpenAIModel: fc.OpenAI.resolvedModel(),
OpenAITemperature: fc.OpenAI.Temperature,
}
out.mergeProviderFields(&amp;tmp)
}</span>
// copilot
- <span class="cov5" title="16">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{
+ <span class="cov5" title="22">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{
tmp := App{
CopilotBaseURL: fc.Copilot.BaseURL,
CopilotModel: fc.Copilot.Model,
@@ -641,7 +670,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
}</span>
// ollama
- <span class="cov5" title="16">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{
+ <span class="cov5" title="22">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{
tmp := App{
OllamaBaseURL: fc.Ollama.BaseURL,
OllamaModel: fc.Ollama.Model,
@@ -652,7 +681,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
// prompts
// completion
- <span class="cov5" title="16">if (fc.Prompts.Completion != sectionPromptsCompletion{}) </span><span class="cov1" title="1">{
+ <span class="cov5" title="22">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>
@@ -673,11 +702,11 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
}</span>
}
// chat
- <span class="cov5" title="16">if strings.TrimSpace(fc.Prompts.Chat.System) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="22">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="16">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" ||
+ <span class="cov5" title="22">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) != "" ||
@@ -687,39 +716,39 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" ||
strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" ||
strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" ||
- len(fc.Prompts.CodeAction.Custom) &gt; 0 </span><span class="cov4" title="12">{
+ len(fc.Prompts.CodeAction.Custom) &gt; 0 </span><span class="cov4" title="17">{
if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" </span><span class="cov1" title="1">{
out.PromptCodeActionRewriteSystem = fc.Prompts.CodeAction.RewriteSystem
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" </span><span class="cov1" title="1">{
out.PromptCodeActionDiagnosticsSystem = fc.Prompts.CodeAction.DiagnosticsSystem
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" </span><span class="cov1" title="1">{
out.PromptCodeActionDocumentSystem = fc.Prompts.CodeAction.DocumentSystem
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" </span><span class="cov1" title="1">{
out.PromptCodeActionRewriteUser = fc.Prompts.CodeAction.RewriteUser
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" </span><span class="cov1" title="1">{
out.PromptCodeActionDiagnosticsUser = fc.Prompts.CodeAction.DiagnosticsUser
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" </span><span class="cov1" title="1">{
out.PromptCodeActionDocumentUser = fc.Prompts.CodeAction.DocumentUser
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" </span><span class="cov1" title="1">{
out.PromptCodeActionGoTestSystem = fc.Prompts.CodeAction.GoTestSystem
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" </span><span class="cov1" title="1">{
out.PromptCodeActionGoTestUser = fc.Prompts.CodeAction.GoTestUser
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" </span><span class="cov0" title="0">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" </span><span class="cov0" title="0">{
out.PromptCodeActionSimplifySystem = fc.Prompts.CodeAction.SimplifySystem
}</span>
- <span class="cov4" title="12">if strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" </span><span class="cov0" title="0">{
+ <span class="cov4" title="17">if strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" </span><span class="cov0" title="0">{
out.PromptCodeActionSimplifyUser = fc.Prompts.CodeAction.SimplifyUser
}</span>
- <span class="cov4" title="12">if len(fc.Prompts.CodeAction.Custom) &gt; 0 </span><span class="cov4" title="11">{
- for _, ca := range fc.Prompts.CodeAction.Custom </span><span class="cov5" title="20">{
+ <span class="cov4" title="17">if len(fc.Prompts.CodeAction.Custom) &gt; 0 </span><span class="cov4" title="16">{
+ for _, ca := range fc.Prompts.CodeAction.Custom </span><span class="cov5" title="30">{
out.CustomActions = append(out.CustomActions, CustomAction{
ID: strings.TrimSpace(ca.ID),
Title: strings.TrimSpace(ca.Title),
@@ -734,7 +763,7 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
}
}
// cli
- <span class="cov5" title="16">if (fc.Prompts.CLI != sectionPromptsCLI{}) </span><span class="cov1" title="1">{
+ <span class="cov5" title="22">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>
@@ -743,46 +772,46 @@ func (fc *fileConfig) toApp() App <span class="cov5" title="16">{
}</span>
}
// provider-native
- <span class="cov5" title="16">if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" </span><span class="cov1" title="1">{
+ <span class="cov5" title="22">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="16">if (fc.Tmux != sectionTmux{}) </span><span class="cov2" title="3">{
+ <span class="cov5" title="22">if (fc.Tmux != sectionTmux{}) </span><span class="cov2" title="3">{
out.TmuxCustomMenuHotkey = strings.TrimSpace(fc.Tmux.CustomMenuHotkey)
}</span>
// stats
- <span class="cov5" title="16">if fc.Stats.WindowMinutes &gt; 0 </span><span class="cov0" title="0">{
+ <span class="cov5" title="22">if fc.Stats.WindowMinutes &gt; 0 </span><span class="cov0" title="0">{
out.StatsWindowMinutes = fc.Stats.WindowMinutes
}</span>
- <span class="cov5" title="16">return out</span>
+ <span class="cov5" title="22">return out</span>
}
-func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="22">{
+func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="28">{
b, err := os.ReadFile(path)
- if err != nil </span><span class="cov3" title="4">{
+ if err != nil </span><span class="cov2" title="4">{
if !os.IsNotExist(err) &amp;&amp; logger != nil </span><span class="cov0" title="0">{
logger.Printf("cannot open TOML config file %s: %v", path, err)
}</span>
- <span class="cov3" title="4">return nil, err</span>
+ <span class="cov2" title="4">return nil, err</span>
}
- <span class="cov5" title="18">var tables fileConfig
+ <span class="cov5" title="24">var tables fileConfig
errTables := toml.NewDecoder(strings.NewReader(string(b))).Decode(&amp;tables)
// Raw map for validation/presence checks
var raw map[string]any
_ = toml.Unmarshal(b, &amp;raw)
- if errTables != nil </span><span class="cov2" title="2">{
- if logger != nil </span><span class="cov2" title="2">{
+ if errTables != nil </span><span class="cov1" title="2">{
+ if logger != nil </span><span class="cov1" title="2">{
logger.Printf("invalid TOML config file %s: %v", path, errTables)
}</span>
- <span class="cov2" title="2">return nil, errTables</span>
+ <span class="cov1" title="2">return nil, errTables</span>
}
// Reject legacy flat keys at top-level (sectioned-only config is allowed)
- <span class="cov5" title="16">legacy := map[string]struct{}{
+ <span class="cov5" title="22">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": {},
@@ -791,8 +820,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="41">{
- if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable </span><span class="cov6" title="38">{
+ 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">{
continue</span>
}
<span class="cov2" title="3">if _, isLegacy := legacy[k]; isLegacy </span><span class="cov0" title="0">{
@@ -800,13 +829,13 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co
}</span>
}
- <span class="cov5" title="16">if logger != nil </span><span class="cov5" title="16">{
+ <span class="cov5" title="22">if logger != nil </span><span class="cov5" title="22">{
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="16">tab := tables.toApp()
+ <span class="cov5" title="22">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">{
@@ -820,7 +849,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co
}
}
}
- <span class="cov5" title="16">if t, ok := raw["logging"].(map[string]any); ok </span><span class="cov2" title="3">{
+ <span class="cov5" title="22">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">
@@ -832,10 +861,10 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co
}
}
}
- <span class="cov5" title="16">return &amp;tab, nil</span>
+ <span class="cov5" title="22">return &amp;tab, nil</span>
}
-func (a *App) mergeWith(other *App) <span class="cov5" title="28">{
+func (a *App) mergeWith(other *App) <span class="cov5" title="27">{
a.mergeBasics(other)
a.mergeProviderFields(other)
a.mergePrompts(other)
@@ -873,95 +902,95 @@ func (a *App) mergeBasics(other *App) <span class="cov6" title="43">{
<span class="cov6" title="43">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="cov2" title="2">{
+ <span class="cov6" title="43">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="cov2" title="2">{
+ <span class="cov6" title="43">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="cov2" title="2">{
+ <span class="cov6" title="43">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="cov2" title="2">{
+ <span class="cov6" title="43">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="cov3" title="7">{
+ <span class="cov6" title="43">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov4" title="13">{
a.Provider = s
}</span>
<