diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-14 23:40:26 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-14 23:40:26 +0300 |
| commit | f4470bbcfbe3b14c99baeef475fe872825a13a39 (patch) | |
| tree | e12fc6168d21119dfff3a0fef5b6c5b54149f3ab | |
| parent | 68438c98d23545ff791768e3e219cd21d3814e0c (diff) | |
release: v0.10.0v0.10.0
| -rw-r--r-- | PLAN.md | 34 | ||||
| -rw-r--r-- | config.toml.example | 21 | ||||
| -rw-r--r-- | docs/coverage.html | 2904 | ||||
| -rw-r--r-- | docs/coverage.out | 31890 | ||||
| -rw-r--r-- | docs/custom-code-actions.md | 136 | ||||
| -rw-r--r-- | internal/appconfig/config.go | 148 | ||||
| -rw-r--r-- | internal/appconfig/config_test.go | 128 | ||||
| -rw-r--r-- | internal/appconfig/custom_validation_more_test.go | 48 | ||||
| -rw-r--r-- | internal/hexaiaction/custom_exec_more_test.go | 48 | ||||
| -rw-r--r-- | internal/hexaiaction/custom_exec_test.go | 29 | ||||
| -rw-r--r-- | internal/hexaiaction/prompts.go | 15 | ||||
| -rw-r--r-- | internal/hexaiaction/run.go | 20 | ||||
| -rw-r--r-- | internal/hexaiaction/tui_custom.go | 76 | ||||
| -rw-r--r-- | internal/hexaiaction/tui_custom_test.go | 77 | ||||
| -rw-r--r-- | internal/hexailsp/run.go | 23 | ||||
| -rw-r--r-- | internal/lsp/codeaction_custom_errors_test.go | 92 | ||||
| -rw-r--r-- | internal/lsp/codeaction_custom_test.go | 110 | ||||
| -rw-r--r-- | internal/lsp/handlers_codeaction.go | 108 | ||||
| -rw-r--r-- | internal/lsp/server.go | 21 | ||||
| -rw-r--r-- | internal/version.go | 2 |
20 files changed, 20076 insertions, 15854 deletions
@@ -0,0 +1,34 @@ +Hexai: Custom Code Actions — Implementation Plan + +Status legend: [ ] pending, [x] done, [~] in progress + +1) Baseline + Design setup + - [x] Add design doc and sample config updates + - [x] Establish baseline test/coverage command + +2) App config: model + parsing + validation + - [x] Add App fields, TOML parsing for [[prompts.code_action.custom]] and [tmux] + - [x] Implement App.Validate() and wire into LSP + tmux startup + - [x] Unit tests: parsing, validation, duplicate detection, hotkey conflicts + +3) LSP: list/resolve custom actions + - [x] Extend ServerOptions + Server to carry custom actions + - [x] List custom actions (scope-aware) in codeAction + - [x] Resolve custom actions (instruction or system+user) + - [x] Unit tests: list includes, resolve edits for selection/diagnostics + +4) Tmux action: submenu + execution + - [x] Add configurable “Custom actions…” menu item with [tmux].custom_menu_hotkey + - [x] Implement submenu to pick a custom action (with per-item hotkeys) + - [x] Implement runCustom() execution path + - [x] Unit tests: runCustom path + - [x] Unit tests: submenu hotkeys and selection via seam + +5) Polish + - [x] Update docs and finalize examples + - [x] All unit tests pass + - [ ] gofumpt formatting on modified Go files + - [ ] (Optional) Raise coverage further with additional tests + +Notes: +- After each numbered section is completed, run tests and verify coverage >= 85% before moving to the next. diff --git a/config.toml.example b/config.toml.example index d9ed8ee..c237d5b 100644 --- a/config.toml.example +++ b/config.toml.example @@ -76,6 +76,27 @@ temperature = 0.2 # simplify_system = "You are a precise code improvement engine. Simplify and improve the given code while preserving behavior. Return only the improved code with no prose or backticks." # simplify_user = "Improve this code:\n{{selection}}" +# Define additional custom code actions (optional) +# [[prompts.code_action.custom]] +# id = "extract-function" # required, unique slug (case-insensitive) +# title = "Extract function" # required, appears in LSP and tmux +# kind = "refactor.extract" # optional (default: "refactor") +# scope = "selection" # optional: selection | diagnostics (default: selection) +# hotkey = "e" # optional, single character for tmux submenu +# instruction = "Extract selected code into a new function named 'extracted' and replace with a call. Return only code, no backticks." + +# [[prompts.code_action.custom]] +# id = "fix-lints" +# title = "Fix linters" +# kind = "quickfix" +# scope = "diagnostics" +# hotkey = "l" +# system = "You are a precise code fixer. Only change selected code." +# user = "Diagnostics to resolve (selection only):\n{{diagnostics}}\n\nSelected code:\n{{selection}}" + [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." + +[tmux] +# custom_menu_hotkey = "a" # hotkey to open the custom actions submenu in hexai-tmux-action diff --git a/docs/coverage.html b/docs/coverage.html index 059834b..2d72d59 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 (90.8%)</option> <option value="file4">codeberg.org/snonux/hexai/internal/editor/editor.go (58.3%)</option> @@ -69,63 +69,67 @@ <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 (85.0%)</option> + <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (87.5%)</option> - <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (69.2%)</option> + <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (67.2%)</option> <option value="file9">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (65.5%)</option> - <option value="file10">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (100.0%)</option> + <option value="file10">codeberg.org/snonux/hexai/internal/hexaiaction/tui_custom.go (83.8%)</option> - <option value="file11">codeberg.org/snonux/hexai/internal/hexaicli/run.go (88.7%)</option> + <option value="file11">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (100.0%)</option> - <option value="file12">codeberg.org/snonux/hexai/internal/hexailsp/run.go (92.5%)</option> + <option value="file12">codeberg.org/snonux/hexai/internal/hexaicli/run.go (88.6%)</option> - <option value="file13">codeberg.org/snonux/hexai/internal/llm/copilot.go (82.4%)</option> + <option value="file13">codeberg.org/snonux/hexai/internal/hexailsp/run.go (83.7%)</option> - <option value="file14">codeberg.org/snonux/hexai/internal/llm/ollama.go (89.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/openai.go (85.5%)</option> + <option value="file15">codeberg.org/snonux/hexai/internal/llm/ollama.go (89.8%)</option> - <option value="file16">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option> + <option value="file16">codeberg.org/snonux/hexai/internal/llm/openai.go (85.5%)</option> - <option value="file17">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option> + <option value="file17">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option> - <option value="file18">codeberg.org/snonux/hexai/internal/llmutils/client.go (100.0%)</option> + <option value="file18">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option> - <option value="file19">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option> + <option value="file19">codeberg.org/snonux/hexai/internal/llmutils/client.go (100.0%)</option> - <option value="file20">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option> + <option value="file20">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option> - <option value="file21">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option> + <option value="file21">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option> - <option value="file22">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> + <option value="file22">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option> - <option value="file23">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.9%)</option> + <option value="file23">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> - <option value="file24">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (78.8%)</option> + <option value="file24">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.9%)</option> - <option value="file25">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.6%)</option> + <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_document.go (88.9%)</option> + <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.9%)</option> - <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> + <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> - <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (66.7%)</option> + <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> - <option value="file29">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (89.1%)</option> + <option value="file29">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (63.6%)</option> - <option value="file30">codeberg.org/snonux/hexai/internal/lsp/server.go (82.6%)</option> + <option value="file30">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (89.4%)</option> - <option value="file31">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> + <option value="file31">codeberg.org/snonux/hexai/internal/lsp/server.go (81.8%)</option> - <option value="file32">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option> + <option value="file32">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> - <option value="file33">codeberg.org/snonux/hexai/internal/textutil/textutil.go (89.0%)</option> + <option value="file33">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option> - <option value="file34">codeberg.org/snonux/hexai/internal/tmux/status.go (66.7%)</option> + <option value="file34">codeberg.org/snonux/hexai/internal/textutil/human.go (92.3%)</option> - <option value="file35">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option> + <option value="file35">codeberg.org/snonux/hexai/internal/textutil/textutil.go (90.4%)</option> + + <option value="file36">codeberg.org/snonux/hexai/internal/tmux/status.go (68.5%)</option> + + <option value="file37">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option> </select> </div> @@ -178,33 +182,32 @@ func main() <span class="cov8" title="1">{ <pre class="file" id="file1" style="display: none">package main import ( - "context" - "flag" - "fmt" - "os" + "context" + "flag" + "fmt" + "os" - "codeberg.org/snonux/hexai/internal/hexaiaction" + "codeberg.org/snonux/hexai/internal/hexaiaction" ) func main() <span class="cov0" title="0">{ - infile := flag.String("infile", "", "Read input from this file instead of stdin") - outfile := flag.String("outfile", "", "Write output to this file instead of stdout") - uiChild := flag.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically") - tmuxTarget := flag.String("tmux-target", "", "tmux split target (advanced)") - tmuxSplit := flag.String("tmux-split", "v", "tmux split orientation: v or h") - tmuxPercent := flag.Int("tmux-percent", 33, "tmux split size percentage (1-100)") - flag.Parse() - - opts := hexaiaction.Options{ - Infile: *infile, Outfile: *outfile, - UIChild: *uiChild, TmuxTarget: *tmuxTarget, TmuxSplit: *tmuxSplit, TmuxPercent: *tmuxPercent, - } - if err := hexaiaction.RunCommand(context.Background(), opts, os.Stdin, os.Stdout, os.Stderr); err != nil </span><span class="cov0" title="0">{ - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - }</span> -} + infile := flag.String("infile", "", "Read input from this file instead of stdin") + outfile := flag.String("outfile", "", "Write output to this file instead of stdout") + uiChild := flag.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically") + tmuxTarget := flag.String("tmux-target", "", "tmux split target (advanced)") + tmuxSplit := flag.String("tmux-split", "v", "tmux split orientation: v or h") + tmuxPercent := flag.Int("tmux-percent", 33, "tmux split size percentage (1-100)") + flag.Parse() + opts := hexaiaction.Options{ + Infile: *infile, Outfile: *outfile, + UIChild: *uiChild, TmuxTarget: *tmuxTarget, TmuxSplit: *tmuxSplit, TmuxPercent: *tmuxPercent, + } + if err := hexaiaction.RunCommand(context.Background(), opts, os.Stdin, os.Stdout, os.Stderr); err != nil </span><span class="cov0" title="0">{ + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + }</span> +} </pre> <pre class="file" id="file2" style="display: none">// Summary: Hexai CLI entrypoint; parses flags and delegates to internal/hexaicli. @@ -310,21 +313,37 @@ type App struct { // 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:"-"` - PromptCodeActionSimplifySystem string `json:"-" toml:"-"` - PromptCodeActionSimplifyUser 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:"-"` + PromptCodeActionSimplifySystem string `json:"-" toml:"-"` + PromptCodeActionSimplifyUser string `json:"-" toml:"-"` // CLI PromptCLIDefaultSystem string `json:"-" toml:"-"` PromptCLIExplainSystem string `json:"-" toml:"-"` + + // Custom code actions and tmux integration + CustomActions []CustomAction `json:"-" toml:"-"` + TmuxCustomMenuHotkey string `json:"-" toml:"-"` +} + +// CustomAction describes a user-defined code action. +type CustomAction struct { + ID string + Title string + Kind string // optional; default "refactor" + Scope string // "selection" (default) | "diagnostics" + Hotkey string // optional, used by tmux submenu + Instruction string // optional; if set and User is empty, use global rewrite templates + System string // optional; used only when User is set + User string // optional; if set, render with available vars } // Constructor: defaults for App (kept first among functions) -func newDefaultConfig() App <span class="cov5" title="19">{ +func newDefaultConfig() App <span class="cov5" title="30">{ // Coding-friendly default temperature across providers // Users can override per provider in config.toml (including 0.0). t := 0.2 @@ -365,10 +384,10 @@ func newDefaultConfig() App <span class="cov5" title="19">{ 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}}", - PromptCodeActionSimplifySystem: "You are a precise code improvement engine. Simplify and improve the given code while preserving behavior. Return only the improved code with no prose or backticks.", - PromptCodeActionSimplifyUser: "Improve 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}}", + PromptCodeActionSimplifySystem: "You are a precise code improvement engine. Simplify and improve the given code while preserving behavior. Return only the improved code with no prose or backticks.", + PromptCodeActionSimplifyUser: "Improve this code:\n{{selection}}", 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.", @@ -377,18 +396,18 @@ func newDefaultConfig() App <span class="cov5" title="19">{ // 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="18">{ +func Load(logger *log.Logger) App <span class="cov5" title="29">{ cfg := newDefaultConfig() - if logger == nil </span><span class="cov3" title="4">{ + if logger == nil </span><span class="cov4" title="8">{ return cfg // Return defaults if no logger is provided (e.g. in tests) }</span> - <span class="cov5" title="14">configPath, err := getConfigPath() + <span class="cov5" title="21">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="14"> { - if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="4">{ + }</span> else<span class="cov5" title="21"> { + if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov4" title="11">{ cfg.mergeWith(fileCfg) }</span> // When the config file is missing or invalid, we keep defaults and still @@ -396,10 +415,10 @@ func Load(logger *log.Logger) App <span class="cov5" title="18">{ } // Environment overrides (take precedence over file) - <span class="cov5" title="14">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ + <span class="cov5" title="21">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ cfg.mergeWith(envCfg) }</span> - <span class="cov5" title="14">return cfg</span> + <span class="cov5" title="21">return cfg</span> } // Private helpers @@ -417,6 +436,7 @@ type fileConfig struct { Copilot sectionCopilot `toml:"copilot"` Ollama sectionOllama `toml:"ollama"` Prompts sectionPrompts `toml:"prompts"` + Tmux sectionTmux `toml:"tmux"` } type sectionGeneral struct { @@ -496,16 +516,17 @@ type sectionPromptsChat struct { } 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"` - SimplifySystem string `toml:"simplify_system"` - SimplifyUser string `toml:"simplify_user"` + 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"` + SimplifySystem string `toml:"simplify_system"` + SimplifyUser string `toml:"simplify_user"` + Custom []sectionCustomAction `toml:"custom"` } type sectionPromptsCLI struct { @@ -517,7 +538,22 @@ type sectionPromptsProviderNative struct { Completion string `toml:"completion"` } -func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ +type sectionCustomAction struct { + ID string `toml:"id"` + Title string `toml:"title"` + Kind string `toml:"kind"` + Scope string `toml:"scope"` + Hotkey string `toml:"hotkey"` + Instruction string `toml:"instruction"` + System string `toml:"system"` + User string `toml:"user"` +} + +type sectionTmux struct { + CustomMenuHotkey string `toml:"custom_menu_hotkey"` +} + +func (fc *fileConfig) toApp() App <span class="cov4" title="11">{ out := App{} // Merge section: general @@ -533,13 +569,13 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ }</span> // logging - <span class="cov3" title="4">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{ + <span class="cov4" title="11">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">{ + <span class="cov4" title="11">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{ tmp := App{ CompletionDebounceMs: fc.Completion.CompletionDebounceMs, CompletionThrottleMs: fc.Completion.CompletionThrottleMs, @@ -549,31 +585,31 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ }</span> // triggers - <span class="cov3" title="4">if len(fc.Triggers.TriggerCharacters) > 0 </span><span class="cov2" title="3">{ + <span class="cov4" title="11">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">{ + <span class="cov4" title="11">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">{ + <span class="cov4" title="11">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">{ + <span class="cov4" title="11">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">{ + <span class="cov4" title="11">if (fc.OpenAI != sectionOpenAI{}) || fc.OpenAI.Temperature != nil </span><span class="cov2" title="3">{ tmp := App{ OpenAIBaseURL: fc.OpenAI.BaseURL, OpenAIModel: fc.OpenAI.Model, @@ -583,7 +619,7 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ }</span> // copilot - <span class="cov3" title="4">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{ + <span class="cov4" title="11">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{ tmp := App{ CopilotBaseURL: fc.Copilot.BaseURL, CopilotModel: fc.Copilot.Model, @@ -593,7 +629,7 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ }</span> // ollama - <span class="cov3" title="4">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{ + <span class="cov4" title="11">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{ tmp := App{ OllamaBaseURL: fc.Ollama.BaseURL, OllamaModel: fc.Ollama.Model, @@ -604,7 +640,7 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ // prompts // completion - <span class="cov3" title="4">if (fc.Prompts.Completion != sectionPromptsCompletion{}) </span><span class="cov1" title="1">{ + <span class="cov4" title="11">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> @@ -625,44 +661,68 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ }</span> } // chat - <span class="cov3" title="4">if strings.TrimSpace(fc.Prompts.Chat.System) != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if strings.TrimSpace(fc.Prompts.Chat.System) != "" </span><span class="cov1" title="1">{ out.PromptChatSystem = fc.Prompts.Chat.System }</span> // code action - <span class="cov3" title="4">if (fc.Prompts.CodeAction != sectionPromptsCodeAction{}) </span><span class="cov1" title="1">{ + <span class="cov4" title="11">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) != "" || + strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" || + strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" || + strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" || + strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" || + strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" || + strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" || + len(fc.Prompts.CodeAction.Custom) > 0 </span><span class="cov3" title="7">{ if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" </span><span class="cov1" title="1">{ out.PromptCodeActionRewriteSystem = fc.Prompts.CodeAction.RewriteSystem }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" </span><span class="cov1" title="1">{ out.PromptCodeActionDiagnosticsSystem = fc.Prompts.CodeAction.DiagnosticsSystem }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" </span><span class="cov1" title="1">{ out.PromptCodeActionDocumentSystem = fc.Prompts.CodeAction.DocumentSystem }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" </span><span class="cov1" title="1">{ + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" </span><span class="cov1" title="1">{ out.PromptCodeActionRewriteUser = fc.Prompts.CodeAction.RewriteUser }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" </span><span class="cov1" title="1">{ + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" </span><span class="cov1" title="1">{ out.PromptCodeActionDiagnosticsUser = fc.Prompts.CodeAction.DiagnosticsUser }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" </span><span class="cov1" title="1">{ + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" </span><span class="cov1" title="1">{ out.PromptCodeActionDocumentUser = fc.Prompts.CodeAction.DocumentUser }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" </span><span class="cov1" title="1">{ + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" </span><span class="cov1" title="1">{ out.PromptCodeActionGoTestSystem = fc.Prompts.CodeAction.GoTestSystem }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionGoTestUser = fc.Prompts.CodeAction.GoTestUser - }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" </span><span class="cov0" title="0">{ - out.PromptCodeActionSimplifySystem = fc.Prompts.CodeAction.SimplifySystem - }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" </span><span class="cov0" title="0">{ - out.PromptCodeActionSimplifyUser = fc.Prompts.CodeAction.SimplifyUser - }</span> - } + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionGoTestUser = fc.Prompts.CodeAction.GoTestUser + }</span> + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" </span><span class="cov0" title="0">{ + out.PromptCodeActionSimplifySystem = fc.Prompts.CodeAction.SimplifySystem + }</span> + <span class="cov3" title="7">if strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" </span><span class="cov0" title="0">{ + out.PromptCodeActionSimplifyUser = fc.Prompts.CodeAction.SimplifyUser + }</span> + <span class="cov3" title="7">if len(fc.Prompts.CodeAction.Custom) > 0 </span><span class="cov3" title="6">{ + for _, ca := range fc.Prompts.CodeAction.Custom </span><span class="cov4" title="10">{ + out.CustomActions = append(out.CustomActions, CustomAction{ + ID: strings.TrimSpace(ca.ID), + Title: strings.TrimSpace(ca.Title), + Kind: strings.TrimSpace(ca.Kind), + Scope: strings.ToLower(strings.TrimSpace(ca.Scope)), + Hotkey: strings.TrimSpace(ca.Hotkey), + Instruction: ca.Instruction, + System: ca.System, + User: ca.User, + }) + }</span> + } + } // cli - <span class="cov3" title="4">if (fc.Prompts.CLI != sectionPromptsCLI{}) </span><span class="cov1" title="1">{ + <span class="cov4" title="11">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> @@ -671,14 +731,19 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ }</span> } // provider-native - <span class="cov3" title="4">if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" </span><span class="cov1" title="1">{ out.PromptNativeCompletion = fc.Prompts.ProviderNative.Completion }</span> - <span class="cov3" title="4">return out</span> + // tmux + <span class="cov4" title="11">if (fc.Tmux != sectionTmux{}) </span><span class="cov2" title="3">{ + out.TmuxCustomMenuHotkey = strings.TrimSpace(fc.Tmux.CustomMenuHotkey) + }</span> + + <span class="cov4" title="11">return out</span> } -func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="15">{ +func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="22">{ b, err := os.ReadFile(path) if err != nil </span><span class="cov4" title="9">{ if !os.IsNotExist(err) && logger != nil </span><span class="cov0" title="0">{ @@ -687,7 +752,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co <span class="cov4" title="9">return nil, err</span> } - <span class="cov3" title="6">var tables fileConfig + <span class="cov4" title="13">var tables fileConfig errTables := toml.NewDecoder(strings.NewReader(string(b))).Decode(&tables) // Raw map for validation/presence checks var raw map[string]any @@ -700,7 +765,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="cov3" title="4">legacy := map[string]struct{}{ + <span class="cov4" title="11">legacy := map[string]struct{}{ "max_tokens": {}, "context_mode": {}, "context_window_lines": {}, "max_context_t |
