diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-06 11:57:45 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-06 11:57:45 +0300 |
| commit | a48079fae6bb19d7c931f275901670cd5839ab5c (patch) | |
| tree | 5788a3e8cac34ffca9d39b0c4b5df720e869b578 | |
| parent | fb267966f7840df222338f57023273a993a73c9a (diff) | |
chore(version): bump to 0.6.0; configurable prompts via config + testsv0.6.0
| -rw-r--r-- | PROJECTSTATUS.md | 35 | ||||
| -rw-r--r-- | config.toml.example | 105 | ||||
| -rw-r--r-- | docs/configuration.md | 133 | ||||
| -rw-r--r-- | docs/coverage.html | 1250 | ||||
| -rw-r--r-- | docs/coverage.out | 8482 | ||||
| -rw-r--r-- | internal/appconfig/config.go | 488 | ||||
| -rw-r--r-- | internal/appconfig/config_test.go | 207 | ||||
| -rw-r--r-- | internal/hexaicli/run.go | 42 | ||||
| -rw-r--r-- | internal/hexailsp/run.go | 52 | ||||
| -rw-r--r-- | internal/lsp/build_prompts_table_test.go | 32 | ||||
| -rw-r--r-- | internal/lsp/chat_prompt_test.go | 36 | ||||
| -rw-r--r-- | internal/lsp/codeaction_prompts_test.go | 102 | ||||
| -rw-r--r-- | internal/lsp/completion_messages_test.go | 16 | ||||
| -rw-r--r-- | internal/lsp/document_test.go | 35 | ||||
| -rw-r--r-- | internal/lsp/handlers_codeaction.go | 89 | ||||
| -rw-r--r-- | internal/lsp/handlers_completion.go | 50 | ||||
| -rw-r--r-- | internal/lsp/handlers_document.go | 6 | ||||
| -rw-r--r-- | internal/lsp/handlers_utils.go | 29 | ||||
| -rw-r--r-- | internal/lsp/helpers_more_test.go | 21 | ||||
| -rw-r--r-- | internal/lsp/provider_native_success_test.go | 31 | ||||
| -rw-r--r-- | internal/lsp/server.go | 74 | ||||
| -rw-r--r-- | internal/lsp/testhelper_capture_llm_test.go | 18 | ||||
| -rw-r--r-- | internal/version.go | 2 |
23 files changed, 6768 insertions, 4567 deletions
diff --git a/PROJECTSTATUS.md b/PROJECTSTATUS.md index 4114df6..d26a5a8 100644 --- a/PROJECTSTATUS.md +++ b/PROJECTSTATUS.md @@ -1,8 +1,27 @@ -# Ideas +# Project status + +This documents shows future items and in progress items. Already completed ones are removed from this document as we go. ## Features -### New features +### AI menu + +``` +[keys.normal] +C-p = ":sh hexai --menu" +``` + +Or maybe + +``` +[keys.normal] +C-p = ":sh hexai-action" +``` + + +And then generate a menu with all the code actions hexai-lsp knows of and include hotkeys for each menu item! Also print out a notice that this is a work-around due to limitations in Helix's current LSP UI. + +### More features * [ ] Have all text LLM prompts be configurable. With defaults as of now. * [ ] implement a code action for selected code block the way via a unix pipe as faster access in helix @@ -10,16 +29,8 @@ - the external command should open a menu to select an action (e.g. "format", "refactor", "explain", "test", etc.) and then apply it to the selected code - the external menu can be opened in a separate tmux pane * [ ] Fish and zsh and bash shell integration for command completion? Have access to the current shell history (N last commands), current directory content, and current directory name. -* [X] Create "generate unit test" code action for selected code block => write test to FILE_test.go file -* [X] Use hexai as a gh copilot... CLI replacemant for command line questions -* [X] Resolve diagnostics code action feature -* [X] LSP server to be used with the Helix text editor -* [X] Code completion using LLMs -* [X] Text completion in general -* [/] Be a replacement for 'github copilot cli' -* [X] Be able to perform inline chats (keeping history in the document) -* [X] Fine tune when Large Language Model (LLM) completions trigger, as it seems that there are some cases where the Large Language Model (LLM) receives a request but Helix isn't suggesting any completions. There seems to be something odd with the in logic. Investigate the TriggerChar logic and make sure it matches Helix's expectations. -* [X] Can anything else can be done with LSP? +* [ ] Be able to re-configure the temperature in-editor +* [ ] Unit test config loading Be able to switch LLMs. diff --git a/config.toml.example b/config.toml.example index 70b4442..3b331c3 100644 --- a/config.toml.example +++ b/config.toml.example @@ -1,34 +1,79 @@ +# Hexai sectioned config example + +[general] max_tokens = 4000 -context_mode = "file-on-new-func" -context_window_lines = 120 max_context_tokens = 4000 -log_preview_limit = 100 -completion_debounce_ms = 200 -completion_throttle_ms = 0 -# Optional: disable disk IO while building context (reserved) -# no_disk_io = true +context_mode = "always-full" # minimal | window | file-on-new-func | always-full +context_window_lines = 120 +coding_temperature = 0.2 # single knob for LSP calls (optional) + +[logging] +# Flatten to: log_preview_limit +log_preview_limit = 100 # chars shown in log previews + +[completion] +completion_debounce_ms = 200 # idle ms before sending a request +completion_throttle_ms = 0 # min ms between requests (0 disables) +manual_invoke_min_prefix = 0 # required identifier chars for manual invoke + +[triggers] trigger_characters = [".", ":", "/", "_", " "] -inline_open = ">" -inline_close = ">" -chat_suffix = ">" -chat_prefixes = ["?", "!", ":", ";"] -coding_temperature = 0.2 - -# Provider: openai | copilot | ollama -provider = "openai" - -# OpenAI -openai_model = "gpt-4.1" -openai_base_url = "https://api.openai.com/v1" -openai_temperature = 0.2 - -# Ollama -ollama_model = "qwen3-coder:30b-a3b-q4_K_M" -ollama_base_url = "http://localhost:11434" -ollama_temperature = 0.2 - -# GitHub Copilot -copilot_model = "gpt-4o-mini" -copilot_base_url = "https://api.githubcopilot.com" -copilot_temperature = 0.2 +[inline] +inline_open = ">" # single-character +inline_close = ">" # single-character + +[chat] +chat_suffix = ">" # single-character +chat_prefixes = ["?", "!", ":", ";"] # single-character items + +[provider] +name = "openai" # openai | copilot | ollama + +[openai] +model = "gpt-4.1" +base_url = "https://api.openai.com/v1" +temperature = 0.2 + +[copilot] +model = "gpt-4o-mini" +base_url = "https://api.githubcopilot.com" +temperature = 0.2 + +[ollama] +model = "qwen3-coder:30b-a3b-q4_K_M" +base_url = "http://localhost:11434" +temperature = 0.2 + +# Prompt templates (optional). Leave commented to use defaults. +[prompts] + +[prompts.completion] +# Templates support {{file}}, {{function}}, {{above}}, {{current}}, {{below}}, {{char}} +# and for additional context: {{context}} +# system_general = "You are a terse code completion engine. Return only the code to insert, no surrounding prose or backticks. Only continue from the cursor; never repeat characters already present to the left of the cursor on the current line (e.g., if 'name :=' is already typed, only return the right-hand side expression)." +# system_params = "You are a code completion engine for function signatures. Return only the parameter list contents (without parentheses), no braces, no prose. Prefer idiomatic names and types." +# system_inline = "You are a precise code completion/refactoring engine. Output only the code to insert with no prose, no comments, and no backticks. Return raw code only." +# user_general = "Provide the next likely code to insert at the cursor.\nFile: {{file}}\nFunction/context: {{function}}\nAbove line: {{above}}\nCurrent line (cursor at character {{char}}): {{current}}\nBelow line: {{below}}\nOnly return the completion snippet." +# user_params = "Cursor is inside the function parameter list. Suggest only the parameter list (no parentheses).\nFunction line: {{function}}\nCurrent line (cursor at {{char}}): {{current}}" +# additional_context = "Additional context:\n{{context}}" + +[prompts.provider_native] +# completion = "// Path: {{path}}\n{{before}}" + +[prompts.chat] +# system = "You are a helpful coding assistant. Answer concisely and clearly." + +[prompts.code_action] +# rewrite_system = "You are a precise code refactoring engine. Rewrite the given code strictly according to the instruction. Return only the updated code with no prose or backticks. Preserve formatting where reasonable." +# diagnostics_system = "You are a precise code fixer. Resolve the given diagnostics by editing only the selected code. Return only the corrected code with no prose or backticks. Keep behavior and style, and avoid unrelated changes." +# document_system = "You are a precise code documentation engine. Add idiomatic documentation comments to the given code. Preserve exact behavior and formatting as much as possible. Return only the updated code with comments, no prose or backticks." +# rewrite_user = "Instruction: {{instruction}}\n\nSelected code to transform:\n{{selection}}" +# diagnostics_user = "Diagnostics to resolve (selection only):\n{{diagnostics}}\n\nSelected code:\n{{selection}}" +# document_user = "Add documentation comments to this code:\n{{selection}}" +# go_test_system = "You are a precise Go unit test generator. Given a Go function, write one or more Test* functions using the testing package. Do NOT include package or imports, only the test function(s). Prefer table-driven tests. Keep it minimal and idiomatic." +# go_test_user = "Function under test:\n{{function}}" + +[prompts.cli] +# default_system = "You are Hexai CLI. Default to very short, concise answers. If the user asks for commands, output only the commands (one per line) with no commentary or explanation. Only when the word 'explain' appears in the prompt, produce a verbose explanation." +# explain_system = "You are Hexai CLI. The user requested an explanation. Provide a clear, verbose explanation with reasoning and details. If commands are needed, include them with brief context." diff --git a/docs/configuration.md b/docs/configuration.md index d52323c..d6c3f4b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,84 +1,16 @@ # Hexai configuration -This document covers all configuration options for Hexai, including the config file, -environment overrides, provider selection, and temperature behavior. +This page explains where the config lives and how to choose a style; the authoritative list of options and comments lives in the example file. -## Config file - -The config file is optional. +Config file - Location: `$XDG_CONFIG_HOME/hexai/config.toml` (usually `~/.config/hexai/config.toml`). -- Example: - -```toml -max_tokens = 4000 -context_mode = "always-full" -context_window_lines = 120 -max_context_tokens = 4000 -log_preview_limit = 100 -completion_debounce_ms = 200 -completion_throttle_ms = 0 -# no_disk_io is reserved for future use -trigger_characters = [".", ":", "/", "_", " "] -inline_open = ">" -inline_close = ">" -chat_suffix = ">" -chat_prefixes = ["?", "!", ":", ";"] -coding_temperature = 0.2 - -# choose one provider: openai | copilot | ollama -provider = "ollama" - -copilot_model = "gpt-4o-mini" -copilot_base_url = "https://api.githubcopilot.com" -copilot_temperature = 0.2 - -openai_model = "gpt-4.1" -openai_base_url = "https://api.openai.com/v1" -openai_temperature = 0.2 - -ollama_model = "qwen3-coder:30b-a3b-q4_K_M" -ollama_base_url = "http://localhost:11434" -ollama_temperature = 0.2 -``` - -Key fields: - -- max_tokens: upper bound for a single LLM response. -- context_mode: `minimal` | `window` | `file-on-new-func` | `always-full`. -- context_window_lines: line count for `window` mode. -- max_context_tokens: hard cap for sent context tokens. -- log_preview_limit: max characters of context preview logged. -- completion_debounce_ms: minimum idle time before sending completion requests. -- completion_throttle_ms: minimum spacing between completion requests (0 disables). -- manual_invoke_min_prefix: minimum typed identifier chars required for manual invoke to proceed without structural triggers (0 allows always). -- no_disk_io: avoid reading files from disk when building context. -- trigger_characters: LSP completion trigger characters. -- inline_open / inline_close: characters that bracket inline prompts (default `>`/`>`). Inline prompts support `>text>` and a double-open variant `>>text>`. Single-character markers are required. -- chat_suffix / chat_prefixes: in-editor chat triggers (default suffix `>` and prefixes `["?","!",":",";"]`). A line ending with one of these prefixes immediately followed by the suffix triggers a chat reply (e.g., `What?>`). Prefixes must be single characters. -- coding_temperature: optional override for LSP calls. -- provider: `openai` | `copilot` | `ollama`. - -### Trigger customization - -Defaults use `>` for inline prompts and chat suffix. You can change them, e.g.: - -```toml -inline_open = "<" -inline_close = ">" -chat_suffix = "/" -chat_prefixes = ["?", "!"] -trigger_characters = [".", ":", "/", "_", " "] -``` - -Notes: -- `inline_open`/`inline_close` must be single characters; `>>text>` is the double‑open variant. -- `chat_prefixes` items must be single characters. +- Style: sectioned tables only — see `config.toml.example` for a complete, commented reference. -## Environment overrides +Environment overrides -- All config-file options can be overridden by environment variables prefixed with `HEXAI_`. -- Env values take precedence over `config.toml`. +- All options can be overridden by environment variables prefixed with `HEXAI_`. +- Env values take precedence over the config file. - Examples: - `HEXAI_PROVIDER`, `HEXAI_MAX_TOKENS`, `HEXAI_CONTEXT_MODE`, `HEXAI_CONTEXT_WINDOW_LINES`, `HEXAI_MAX_CONTEXT_TOKENS`, `HEXAI_LOG_PREVIEW_LIMIT` - `HEXAI_CODING_TEMPERATURE` @@ -95,33 +27,15 @@ API keys: - OpenAI: prefer `HEXAI_OPENAI_API_KEY`, falling back to `OPENAI_API_KEY`. - Copilot: prefer `HEXAI_COPILOT_API_KEY`, falling back to `COPILOT_API_KEY`. -## Selecting a provider +Selecting a provider -- Set `provider` in the config to `openai`, `copilot`, or `ollama`. +- Sectioned: set `[provider] name = "openai" | "copilot" | "ollama"`. +- Flat: set `provider = "openai" | "copilot" | "ollama"`. - If omitted, Hexai defaults to `openai`. -### OpenAI configuration - -- Required: `HEXAI_OPENAI_API_KEY` (or `OPENAI_API_KEY`). -- Options: - - `openai_model` — model name (default: `gpt-4.1`). - - `openai_base_url` — API base (default: `https://api.openai.com/v1`). - - `openai_temperature` — default temperature (coding-friendly `0.2`). - -### GitHub Copilot configuration - -- Required: `COPILOT_API_KEY`. -- Options: - - `copilot_model` — model name (default: `gpt-4o-mini`). - - `copilot_base_url` — API base (default: `https://api.githubcopilot.com`). - - `copilot_temperature` — default temperature (coding-friendly `0.2`). +Provider-specific options -### Ollama configuration - -- Options: - - `ollama_model` — model name/tag (default: `qwen3-coder:30b-a3b-q4_K_M`). - - `ollama_base_url` — base URL (default: `http://localhost:11434`). - - `ollama_temperature` — default temperature (coding-friendly `0.2`). +- See `config.toml.example` for the per-provider tables and defaults. Notes: @@ -129,27 +43,10 @@ Notes: - Alternatively, run Ollama in OpenAI‑compatible mode and use the OpenAI provider with `openai_base_url` pointed at your local endpoint. -## LSP completion tuning - -- Debounce: `completion_debounce_ms` waits until there has been no recent input for at least this many milliseconds before sending a completion request. Recommended 150–300 ms to balance responsiveness and API usage. -- Throttle: `completion_throttle_ms` enforces a minimum spacing between completion requests, across both chat and provider-native paths. Set to 0 to disable. Recommended 300–600 ms if you still see excessive requests with just debounce. -- Manual invoke prefix: `manual_invoke_min_prefix` requires this many identifier characters before a manual completion (TriggerKind=1) proceeds without other triggers. Use 0 to always allow manual invoke. - -Environment variables mirror these settings: `HEXAI_COMPLETION_DEBOUNCE_MS`, `HEXAI_COMPLETION_THROTTLE_MS`, `HEXAI_MANUAL_INVOKE_MIN_PREFIX`. - -## Temperature behavior - -- What it is: controls randomness/creativity of outputs. -- Default for coding: `0.2` for all providers unless overridden. -- Per-provider overrides: `openai_temperature`, `copilot_temperature`, `ollama_temperature`. - -Recommended ranges: +LSP completion tuning -- 0.0–0.3: deterministic and precise; best for refactors, tests, and bug fixes. -- 0.4–0.7: balanced; general Q&A and writing. -- 0.8–1.2+: creative; brainstorming; may increase tangents. +- See the [completion] section in `config.toml.example`. -Guidance: +Temperature behavior -- Lower temperature increases consistency, but can be terse or repetitive. -- Higher temperature increases diversity, but can wander or introduce mistakes. +- Defaults and recommended ranges are commented inline in `config.toml.example` under [general] and provider tables. diff --git a/docs/coverage.html b/docs/coverage.html index 49b89df..d22ef74 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -59,9 +59,9 @@ <option value="file1">codeberg.org/snonux/hexai/cmd/hexai/main.go (0.0%)</option> - <option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (87.0%)</option> + <option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (91.6%)</option> - <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (91.4%)</option> + <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (72.6%)</option> <option value="file4">codeberg.org/snonux/hexai/internal/hexailsp/run.go (92.5%)</option> @@ -83,11 +83,11 @@ <option value="file13">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> - <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.1%)</option> + <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.9%)</option> <option value="file15">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (81.2%)</option> - <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.5%)</option> + <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.6%)</option> <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> @@ -97,7 +97,7 @@ <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (88.5%)</option> - <option value="file21">codeberg.org/snonux/hexai/internal/lsp/server.go (77.9%)</option> + <option value="file21">codeberg.org/snonux/hexai/internal/lsp/server.go (82.1%)</option> <option value="file22">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> @@ -234,17 +234,43 @@ type App struct { // Default temperature for Ollama requests (nil means use provider default) OllamaTemperature *float64 `json:"ollama_temperature" toml:"ollama_temperature"` CopilotBaseURL string `json:"copilot_base_url" toml:"copilot_base_url"` - CopilotModel string `json:"copilot_model" toml:"copilot_model"` - // Default temperature for Copilot requests (nil means use provider default) - CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_temperature"` + CopilotModel string `json:"copilot_model" toml:"copilot_model"` + // Default temperature for Copilot requests (nil means use provider default) + CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_temperature"` + + // Prompt templates (configured only via file; no env overrides) + // Completion/chat/code action/CLI prompt strings. See config.toml.example for placeholders. + // Completion + PromptCompletionSystemGeneral string `json:"-" toml:"-"` + PromptCompletionSystemParams string `json:"-" toml:"-"` + PromptCompletionSystemInline string `json:"-" toml:"-"` + PromptCompletionUserGeneral string `json:"-" toml:"-"` + PromptCompletionUserParams string `json:"-" toml:"-"` + PromptCompletionExtraHeader string `json:"-" toml:"-"` + // Provider-native code-completer + PromptNativeCompletion string `json:"-" toml:"-"` + // In-editor chat + PromptChatSystem string `json:"-" toml:"-"` + // Code actions + PromptCodeActionRewriteSystem string `json:"-" toml:"-"` + PromptCodeActionDiagnosticsSystem string `json:"-" toml:"-"` + PromptCodeActionDocumentSystem string `json:"-" toml:"-"` + PromptCodeActionRewriteUser string `json:"-" toml:"-"` + PromptCodeActionDiagnosticsUser string `json:"-" toml:"-"` + PromptCodeActionDocumentUser string `json:"-" toml:"-"` + PromptCodeActionGoTestSystem string `json:"-" toml:"-"` + PromptCodeActionGoTestUser string `json:"-" toml:"-"` + // CLI + PromptCLIDefaultSystem string `json:"-" toml:"-"` + PromptCLIExplainSystem string `json:"-" toml:"-"` } // Constructor: defaults for App (kept first among functions) -func newDefaultConfig() App <span class="cov5" title="9">{ +func newDefaultConfig() App <span class="cov5" title="11">{ // Coding-friendly default temperature across providers // Users can override per provider in config.toml (including 0.0). t := 0.2 - return App{ + return App{ MaxTokens: 4000, ContextMode: "always-full", ContextWindowLines: 120, @@ -261,24 +287,48 @@ func newDefaultConfig() App <span class="cov5" title="9">{ InlineOpen: ">", InlineClose: ">", ChatSuffix: ">", - ChatPrefixes: []string{"?", "!", ":", ";"}, - } + ChatPrefixes: []string{"?", "!", ":", ";"}, + + // Default prompt templates (match current hard-coded strings) + PromptCompletionSystemParams: "You are a code completion engine for function signatures. Return only the parameter list contents (without parentheses), no braces, no prose. Prefer idiomatic names and types.", + PromptCompletionUserParams: "Cursor is inside the function parameter list. Suggest only the parameter list (no parentheses).\nFunction line: {{function}}\nCurrent line (cursor at {{char}}): {{current}}", + PromptCompletionSystemGeneral: "You are a terse code completion engine. Return only the code to insert, no surrounding prose or backticks. Only continue from the cursor; never repeat characters already present to the left of the cursor on the current line (e.g., if 'name :=' is already typed, only return the right-hand side expression).", + PromptCompletionUserGeneral: "Provide the next likely code to insert at the cursor.\nFile: {{file}}\nFunction/context: {{function}}\nAbove line: {{above}}\nCurrent line (cursor at character {{char}}): {{current}}\nBelow line: {{below}}\nOnly return the completion snippet.", + PromptCompletionSystemInline: "You are a precise code completion/refactoring engine. Output only the code to insert with no prose, no comments, and no backticks. Return raw code only.", + PromptCompletionExtraHeader: "Additional context:\n{{context}}", + + PromptNativeCompletion: "// Path: {{path}}\n{{before}}", + + PromptChatSystem: "You are a helpful coding assistant. Answer concisely and clearly.", + + PromptCodeActionRewriteSystem: "You are a precise code refactoring engine. Rewrite the given code strictly according to the instruction. Return only the updated code with no prose or backticks. Preserve formatting where reasonable.", + PromptCodeActionDiagnosticsSystem: "You are a precise code fixer. Resolve the given diagnostics by editing only the selected code. Return only the corrected code with no prose or backticks. Keep behavior and style, and avoid unrelated changes.", + PromptCodeActionDocumentSystem: "You are a precise code documentation engine. Add idiomatic documentation comments to the given code. Preserve exact behavior and formatting as much as possible. Return only the updated code with comments, no prose or backticks.", + PromptCodeActionRewriteUser: "Instruction: {{instruction}}\n\nSelected code to transform:\n{{selection}}", + PromptCodeActionDiagnosticsUser: "Diagnostics to resolve (selection only):\n{{diagnostics}}\n\nSelected code:\n{{selection}}", + PromptCodeActionDocumentUser: "Add documentation comments to this code:\n{{selection}}", + PromptCodeActionGoTestSystem: "You are a precise Go unit test generator. Given a Go function, write one or more Test* functions using the testing package. Do NOT include package or imports, only the test function(s). Prefer table-driven tests. Keep it minimal and idiomatic.", + PromptCodeActionGoTestUser: "Function under test:\n{{function}}", + + PromptCLIDefaultSystem: "You are Hexai CLI. Default to very short, concise answers. If the user asks for commands, output only the commands (one per line) with no commentary or explanation. Only when the word 'explain' appears in the prompt, produce a verbose explanation.", + PromptCLIExplainSystem: "You are Hexai CLI. The user requested an explanation. Provide a clear, verbose explanation with reasoning and details. If commands are needed, include them with brief context.", + } }</span> // 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="cov4" title="8">{ +func Load(logger *log.Logger) App <span class="cov5" title="10">{ cfg := newDefaultConfig() - if logger == nil </span><span class="cov3" title="3">{ + if logger == nil </span><span class="cov2" title="3">{ return cfg // Return defaults if no logger is provided (e.g. in tests) }</span> - <span class="cov4" title="5">configPath, err := getConfigPath() + <span class="cov4" title="7">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="cov4" title="5"> { - if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="3">{ + }</span> else<span class="cov4" title="7"> { + if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="4">{ cfg.mergeWith(fileCfg) }</span> // When the config file is missing or invalid, we keep defaults and still @@ -286,125 +336,507 @@ func Load(logger *log.Logger) App <span class="cov4" title="8">{ } // Environment overrides (take precedence over file) - <span class="cov4" title="5">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ + <span class="cov4" title="7">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ cfg.mergeWith(envCfg) }</span> - <span class="cov4" title="5">return cfg</span> + <span class="cov4" title="7">return cfg</span> } // Private helpers -func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="6">{ - f, err := os.Open(path) - if err != nil </span><span class="cov2" title="2">{ - if !os.IsNotExist(err) && logger != nil </span><span class="cov0" title="0">{ - logger.Printf("cannot open TOML config file %s: %v", path, err) - }</span> - <span class="cov2" title="2">return nil, err</span> - } - <span class="cov3" title="4">defer f.Close() +// Sectioned (table-based) file format only. +type fileConfig struct { + // Section tables only (flat keys are not allowed) + General sectionGeneral `toml:"general"` + Logging sectionLogging `toml:"logging"` + Completion sectionCompletion `toml:"completion"` + Triggers sectionTriggers `toml:"triggers"` + Inline sectionInline `toml:"inline"` + Chat sectionChat `toml:"chat"` + Provider sectionProvider `toml:"provider"` + OpenAI sectionOpenAI `toml:"openai"` + Copilot sectionCopilot `toml:"copilot"` + Ollama sectionOllama `toml:"ollama"` + Prompts sectionPrompts `toml:"prompts"` +} - dec := toml.NewDecoder(f) - var fileCfg App - if err := dec.Decode(&fileCfg); err != nil </span><span class="cov1" title="1">{ - if logger != nil </span><span class="cov1" title="1">{ - logger.Printf("invalid TOML config file %s: %v", path, err) - }</span> - <span class="cov1" title="1">return nil, err</span> +type sectionGeneral struct { + MaxTokens int `toml:"max_tokens"` + ContextMode string `toml:"context_mode"` + ContextWindowLines int `toml:"context_window_lines"` + MaxContextTokens int `toml:"max_context_tokens"` + CodingTemperature *float64 `toml:"coding_temperature"` +} + +type sectionLogging struct { + LogPreviewLimit int `toml:"log_preview_limit"` +} + +type sectionCompletion struct { + CompletionDebounceMs int `toml:"completion_debounce_ms"` + CompletionThrottleMs int `toml:"completion_throttle_ms"` + ManualInvokeMinPrefix int `toml:"manual_invoke_min_prefix"` +} + +type sectionTriggers struct { + TriggerCharacters []string `toml:"trigger_characters"` +} + +type sectionInline struct { + InlineOpen string `toml:"inline_open"` + InlineClose string `toml:"inline_close"` +} + +type sectionChat struct { + ChatSuffix string `toml:"chat_suffix"` + ChatPrefixes []string `toml:"chat_prefixes"` +} + +type sectionProvider struct { + Name string `toml:"name"` +} + +type sectionOpenAI struct { + Model string `toml:"model"` + BaseURL string `toml:"base_url"` + Temperature *float64 `toml:"temperature"` +} + +type sectionCopilot struct { + Model string `toml:"model"` + BaseURL string `toml:"base_url"` + Temperature *float64 `toml:"temperature"` +} + +type sectionOllama struct { + Model string `toml:"model"` + BaseURL string `toml:"base_url"` + Temperature *float64 `toml:"temperature"` +} + +// Prompts sections +type sectionPrompts struct { + Completion sectionPromptsCompletion `toml:"completion"` + Chat sectionPromptsChat `toml:"chat"` + CodeAction sectionPromptsCodeAction `toml:"code_action"` + CLI sectionPromptsCLI `toml:"cli"` + ProviderNative sectionPromptsProviderNative `toml:"provider_native"` +} + +type sectionPromptsCompletion struct { + SystemGeneral string `toml:"system_general"` + SystemParams string `toml:"system_params"` + SystemInline string `toml:"system_inline"` + UserGeneral string `toml:"user_general"` + UserParams string `toml:"user_params"` + ExtraHeader string `toml:"additional_context"` +} + +type sectionPromptsChat struct { + System string `toml:"system"` +} + +type sectionPromptsCodeAction struct { + RewriteSystem string `toml:"rewrite_system"` + DiagnosticsSystem string `toml:"diagnostics_system"` + DocumentSystem string `toml:"document_system"` + RewriteUser string `toml:"rewrite_user"` + DiagnosticsUser string `toml:"diagnostics_user"` + DocumentUser string `toml:"document_user"` + GoTestSystem string `toml:"go_test_system"` + GoTestUser string `toml:"go_test_user"` +} + +type sectionPromptsCLI struct { + DefaultSystem string `toml:"default_system"` + ExplainSystem string `toml:"explain_system"` +} + +type sectionPromptsProviderNative struct { + Completion string `toml:"completion"` +} + +func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ + out := App{} + + // Merge section: general + if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil </span><span class="cov2" title="3">{ + tmp := App{ + MaxTokens: fc.General.MaxTokens, + ContextMode: fc.General.ContextMode, + ContextWindowLines: fc.General.ContextWindowLines, + MaxContextTokens: fc.General.MaxContextTokens, + CodingTemperature: fc.General.CodingTemperature, } - <span class="cov3" title="3">if logger != nil </span><span class="cov3" title="3">{ - logger.Printf("loaded configuration from %s (TOML)", path) - }</span> - <span class="cov3" title="3">return &fileCfg, nil</span> + out.mergeBasics(&tmp) + }</span> + + // logging + <span class="cov3" title="4">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{ + tmp := App{LogPreviewLimit: fc.Logging.LogPreviewLimit} + out.mergeBasics(&tmp) + }</span> + + // completion + <span class="cov3" title="4">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{ + tmp := App{ + CompletionDebounceMs: fc.Completion.CompletionDebounceMs, + CompletionThrottleMs: fc.Completion.CompletionThrottleMs, + ManualInvokeMinPrefix: fc.Completion.ManualInvokeMinPrefix, + } + out.mergeBasics(&tmp) + }</span> + + // triggers + <span class="cov3" title="4">if len(fc.Triggers.TriggerCharacters) > 0 </span><span class="cov2" title="3">{ + tmp := App{TriggerCharacters: fc.Triggers.TriggerCharacters} + out.mergeBasics(&tmp) + }</span> + + // inline + <span class="cov3" title="4">if (fc.Inline != sectionInline{}) </span><span class="cov1" title="1">{ + tmp := App{InlineOpen: fc.Inline.InlineOpen, InlineClose: fc.Inline.InlineClose} + out.mergeBasics(&tmp) + }</span> + + // chat + <span class="cov3" title="4">if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) > 0 </span><span class="cov1" title="1">{ + tmp := App{ChatSuffix: fc.Chat.ChatSuffix, ChatPrefixes: fc.Chat.ChatPrefixes} + out.mergeBasics(&tmp) + }</span> + + // provider + <span class="cov3" title="4">if strings.TrimSpace(fc.Provider.Name) != "" </span><span class="cov2" title="3">{ + tmp := App{Provider: fc.Provider.Name} + out.mergeBasics(&tmp) + }</span> + + // openai + <span class="cov3" title="4">if (fc.OpenAI != sectionOpenAI{}) || fc.OpenAI.Temperature != nil </span><span class="cov2" title="3">{ + tmp := App{ + OpenAIBaseURL: fc.OpenAI.BaseURL, + OpenAIModel: fc.OpenAI.Model, + OpenAITemperature: fc.OpenAI.Temperature, + } + out.mergeProviderFields(&tmp) + }</span> + + // copilot + <span class="cov3" title="4">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{ + tmp := App{ + CopilotBaseURL: fc.Copilot.BaseURL, + CopilotModel: fc.Copilot.Model, + CopilotTemperature: fc.Copilot.Temperature, + } + out.mergeProviderFields(&tmp) + }</span> + + // ollama + <span class="cov3" title="4">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{ + tmp := App{ + OllamaBaseURL: fc.Ollama.BaseURL, + OllamaModel: fc.Ollama.Model, + OllamaTemperature: fc.Ollama.Temperature, + } + out.mergeProviderFields(&tmp) + }</span> + + // prompts + // completion + <span class="cov3" title="4">if (fc.Prompts.Completion != sectionPromptsCompletion{}) </span><span class="cov1" title="1">{ + if strings.TrimSpace(fc.Pr |
