diff options
Diffstat (limited to 'docs/coverage.html')
| -rw-r--r-- | docs/coverage.html | 642 |
1 files changed, 405 insertions, 237 deletions
diff --git a/docs/coverage.html b/docs/coverage.html index fb5d655..8560a58 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -61,67 +61,69 @@ <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 (91.6%)</option> + <option value="file3">codeberg.org/snonux/hexai/internal/appconfig/config.go (90.6%)</option> - <option value="file4">codeberg.org/snonux/hexai/internal/hexaiaction/cmdentry.go (84.5%)</option> + <option value="file4">codeberg.org/snonux/hexai/internal/editor/editor.go (58.3%)</option> - <option value="file5">codeberg.org/snonux/hexai/internal/hexaiaction/parse.go (92.6%)</option> + <option value="file5">codeberg.org/snonux/hexai/internal/hexaiaction/cmdentry.go (84.5%)</option> - <option value="file6">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (91.9%)</option> + <option value="file6">codeberg.org/snonux/hexai/internal/hexaiaction/parse.go (92.6%)</option> - <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (71.8%)</option> + <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (85.0%)</option> - <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (65.5%)</option> + <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (67.3%)</option> - <option value="file9">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (100.0%)</option> + <option value="file9">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (65.5%)</option> - <option value="file10">codeberg.org/snonux/hexai/internal/hexaicli/run.go (78.8%)</option> + <option value="file10">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (100.0%)</option> - <option value="file11">codeberg.org/snonux/hexai/internal/hexailsp/run.go (92.5%)</option> + <option value="file11">codeberg.org/snonux/hexai/internal/hexaicli/run.go (88.4%)</option> - <option value="file12">codeberg.org/snonux/hexai/internal/llm/copilot.go (82.4%)</option> + <option value="file12">codeberg.org/snonux/hexai/internal/hexailsp/run.go (92.5%)</option> - <option value="file13">codeberg.org/snonux/hexai/internal/llm/ollama.go (89.8%)</option> + <option value="file13">codeberg.org/snonux/hexai/internal/llm/copilot.go (82.4%)</option> - <option value="file14">codeberg.org/snonux/hexai/internal/llm/openai.go (85.5%)</option> + <option value="file14">codeberg.org/snonux/hexai/internal/llm/ollama.go (89.8%)</option> - <option value="file15">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option> + <option value="file15">codeberg.org/snonux/hexai/internal/llm/openai.go (85.5%)</option> - <option value="file16">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option> + <option value="file16">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option> - <option value="file17">codeberg.org/snonux/hexai/internal/llmutils/client.go (100.0%)</option> + <option value="file17">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option> - <option value="file18">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option> + <option value="file18">codeberg.org/snonux/hexai/internal/llmutils/client.go (100.0%)</option> - <option value="file19">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option> + <option value="file19">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option> - <option value="file20">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option> + <option value="file20">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option> - <option value="file21">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> + <option value="file21">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option> - <option value="file22">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.9%)</option> + <option value="file22">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> - <option value="file23">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (81.9%)</option> + <option value="file23">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.9%)</option> - <option value="file24">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.6%)</option> + <option value="file24">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (78.8%)</option> - <option value="file25">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> + <option value="file25">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.6%)</option> - <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> + <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> - <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (66.7%)</option> + <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> - <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (89.0%)</option> + <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (66.7%)</option> - <option value="file29">codeberg.org/snonux/hexai/internal/lsp/server.go (82.1%)</option> + <option value="file29">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (89.0%)</option> - <option value="file30">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> + <option value="file30">codeberg.org/snonux/hexai/internal/lsp/server.go (82.6%)</option> - <option value="file31">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option> + <option value="file31">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> - <option value="file32">codeberg.org/snonux/hexai/internal/textutil/textutil.go (89.0%)</option> + <option value="file32">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option> - <option value="file33">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option> + <option value="file33">codeberg.org/snonux/hexai/internal/textutil/textutil.go (89.0%)</option> + + <option value="file34">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option> </select> </div> @@ -306,19 +308,21 @@ 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:"-"` + 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:"-"` } // Constructor: defaults for App (kept first among functions) -func newDefaultConfig() App <span class="cov5" title="16">{ +func newDefaultConfig() App <span class="cov5" title="19">{ // Coding-friendly default temperature across providers // Users can override per provider in config.toml (including 0.0). t := 0.2 @@ -359,8 +363,10 @@ func newDefaultConfig() App <span class="cov5" title="16">{ 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}}", + 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.", @@ -369,17 +375,17 @@ func newDefaultConfig() App <span class="cov5" title="16">{ // 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="15">{ +func Load(logger *log.Logger) App <span class="cov5" title="18">{ cfg := newDefaultConfig() if logger == nil </span><span class="cov3" title="4">{ return cfg // Return defaults if no logger is provided (e.g. in tests) }</span> - <span class="cov4" title="11">configPath, err := getConfigPath() + <span class="cov5" title="14">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="11"> { + }</span> else<span class="cov5" title="14"> { if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="4">{ cfg.mergeWith(fileCfg) }</span> @@ -388,10 +394,10 @@ func Load(logger *log.Logger) App <span class="cov5" title="15">{ } // Environment overrides (take precedence over file) - <span class="cov4" title="11">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ cfg.mergeWith(envCfg) }</span> - <span class="cov4" title="11">return cfg</span> + <span class="cov5" title="14">return cfg</span> } // Private helpers @@ -488,14 +494,16 @@ 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"` + 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"` } type sectionPromptsCLI struct { @@ -619,7 +627,7 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ 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="cov3" title="4">if (fc.Prompts.CodeAction != sectionPromptsCodeAction{}) </span><span class="cov1" title="1">{ if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" </span><span class="cov1" title="1">{ out.PromptCodeActionRewriteSystem = fc.Prompts.CodeAction.RewriteSystem }</span> @@ -641,10 +649,16 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ <span class="cov1" title="1">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.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> + } // cli <span class="cov3" title="4">if (fc.Prompts.CLI != sectionPromptsCLI{}) </span><span class="cov1" title="1">{ if strings.TrimSpace(fc.Prompts.CLI.DefaultSystem) != "" </span><span class="cov1" title="1">{ @@ -662,13 +676,13 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ <span class="cov3" title="4">return out</span> } -func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="12">{ +func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="15">{ b, err := os.ReadFile(path) - if err != nil </span><span class="cov3" title="6">{ + if err != nil </span><span class="cov4" title="9">{ 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="cov3" title="6">return nil, err</span> + <span class="cov4" title="9">return nil, err</span> } <span class="cov3" title="6">var tables fileConfig @@ -843,9 +857,15 @@ func (a *App) mergePrompts(other *App) <span class="cov3" title="5">{ <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" </span><span class="cov1" title="1">{ a.PromptCodeActionGoTestSystem = other.PromptCodeActionGoTestSystem }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionGoTestUser = other.PromptCodeActionGoTestUser - }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionGoTestUser = other.PromptCodeActionGoTestUser + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionSimplifySystem) != "" </span><span class="cov0" title="0">{ + a.PromptCodeActionSimplifySystem = other.PromptCodeActionSimplifySystem + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionSimplifyUser) != "" </span><span class="cov0" title="0">{ + a.PromptCodeActionSimplifyUser = other.PromptCodeActionSimplifyUser + }</span> // CLI <span class="cov3" title="5">if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" </span><span class="cov1" title="1">{ a.PromptCLIDefaultSystem = other.PromptCLIDefaultSystem @@ -886,33 +906,33 @@ func (a *App) mergeProviderFields(other *App) <span class="cov5" title="14">{ }</span> } -func getConfigPath() (string, error) <span class="cov5" title="12">{ +func getConfigPath() (string, error) <span class="cov5" title="15">{ var configPath string if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov4" title="7">{ configPath = filepath.Join(xdgConfigHome, "hexai", "config.toml") - }</span> else<span class="cov3" title="5"> { + }</span> else<span class="cov4" title="8"> { home, err := os.UserHomeDir() if err != nil </span><span class="cov0" title="0">{ return "", fmt.Errorf("cannot find user home directory: %v", err) }</span> - <span class="cov3" title="5">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span> + <span class="cov4" title="8">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span> } - <span class="cov5" title="12">return configPath, nil</span> + <span class="cov5" title="15">return configPath, nil</span> } // --- Environment overrides --- // loadFromEnv constructs an App containing only fields set via HEXAI_* env vars. // These values should take precedence over file config when merged. -func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="11">{ +func loadFromEnv(logger *log.Logger) *App <span class="cov5" title="14">{ var out App var any bool // helpers - getenv := func(k string) string </span><span class="cov10" title="264">{ return strings.TrimSpace(os.Getenv(k)) }</span> - <span class="cov4" title="11">parseInt := func(k string) (int, bool) </span><span class="cov8" title="77">{ + getenv := func(k string) string </span><span class="cov10" title="336">{ return strings.TrimSpace(os.Getenv(k)) }</span> + <span class="cov5" title="14">parseInt := func(k string) (int, bool) </span><span class="cov8" title="98">{ v := getenv(k) - if v == "" </span><span class="cov7" title="70">{ + if v == "" </span><span class="cov7" title="91">{ return 0, false }</span> <span class="cov4" title="7">n, err := strconv.Atoi(v) @@ -924,9 +944,9 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="11">{ } <span class="cov4" title="7">return n, true</span> } - <span class="cov4" title="11">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="44">{ + <span class="cov5" title="14">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="56">{ v := getenv(k) - if v == "" </span><span class="cov6" title="40">{ + if v == "" </span><span class="cov7" title="52">{ return nil, false }</span> <span class="cov3" title="4">f, err := strconv.ParseFloat(v, 64) @@ -939,43 +959,43 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="11">{ <span class="cov3" title="4">return &f, true</span> } - <span class="cov4" title="11">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ out.MaxTokens = n any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ out.ContextMode = s any = true }</span> - <span class="cov4" title="11">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ out.ContextWindowLines = n any = true }</span> - <span class="cov4" title="11">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ out.MaxContextTokens = n any = true }</span> - <span class="cov4" title="11">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ out.LogPreviewLimit = n any = true }</span> - <span class="cov4" title="11">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ out.ManualInvokeMinPrefix = n any = true }</span> - <span class="cov4" title="11">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ out.CompletionDebounceMs = n any = true }</span> - <span class="cov4" title="11">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ out.CompletionThrottleMs = n any = true }</span> - <span class="cov4" title="11">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CodingTemperature = f any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ parts := strings.Split(s, ",") out.TriggerCharacters = nil for _, p := range parts </span><span class="cov2" title="3">{ @@ -985,19 +1005,19 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="11">{ } <span class="cov1" title="1">any = true</span> } - <span class="cov4" title="11">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="14">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ out.InlineOpen = s any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="14">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ out.InlineClose = s any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="14">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ out.ChatSuffix = s any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ + <span class="cov5" title="14">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ parts := strings.Split(s, ",") out.ChatPrefixes = nil for _, p := range parts </span><span class="cov0" title="0">{ @@ -1007,59 +1027,131 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="11">{ } <span class="cov0" title="0">any = true</span> } - <span class="cov4" title="11">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ out.Provider = s any = true }</span> // Provider-specific - <span class="cov4" title="11">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIBaseURL = s any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIModel = s any = true }</span> - <span class="cov4" title="11">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OpenAITemperature = f any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OllamaBaseURL = s any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OllamaModel = s any = true }</span> - <span class="cov4" title="11">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OllamaTemperature = f any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.CopilotBaseURL = s any = true }</span> - <span class="cov4" title="11">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ out.CopilotModel = s any = true }</span> - <span class="cov4" title="11">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov5" title="14">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CopilotTemperature = f any = true }</span> - <span class="cov4" title="11">if !any </span><span class="cov4" title="10">{ + <span class="cov5" title="14">if !any </span><span class="cov4" title="13">{ return nil }</span> <span class="cov1" title="1">return &out</span> } </pre> - <pre class="file" id="file4" style="display: none">package hexaiaction + <pre class="file" id="file4" style="display: none">package editor + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// Resolve returns the editor command from HEXAI_EDITOR or EDITOR. +func Resolve() (string, error) <span class="cov10" title="5">{ + ed := strings.TrimSpace(os.Getenv("HEXAI_EDITOR")) + if ed == "" </span><span class="cov1" title="1">{ + ed = strings.TrimSpace(os.Getenv("EDITOR")) + }</span> + <span class="cov10" title="5">if ed == "" </span><span class="cov0" title="0">{ + return "", errors.New("no editor configured (set HEXAI_EDITOR or EDITOR)") + }</span> + <span class="cov10" title="5">return ed, nil</span> +} + +// RunEditor is the seam that invokes the editor on the given file path. +// Override in tests to avoid launching a real editor. +var RunEditor = func(editor, path string) error <span class="cov0" title="0">{ + cmd := exec.Command(editor, path) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +}</span> + +// OpenTempAndEdit creates a temporary .md file, writes initial content if provided, +// opens it in the resolved editor, then reads the final content and removes the file. +// Returns the trimmed content. +func OpenTempAndEdit(initial []byte) (string, error) <span class="cov7" title="3">{ + ed, err := Resolve() + if err != nil </span><span class="cov0" title="0">{ + return "", err + }</span> + // Create temp file under system temp dir; ensure .md suffix + <span class="cov7" title="3">dir := os.TempDir() + f, err := os.CreateTemp(dir, "hexai-*.md") + if err != nil </span><span class="cov0" title="0">{ + return "", err + }</span> + <span class="cov7" title="3">path := f.Name() + defer func() </span><span class="cov7" title="3">{ _ = os.Remove(path) }</span>() + <span class="cov7" title="3">if len(initial) > 0 </span><span class="cov1" title="1">{ + if _, err := f.Write(initial); err != nil </span><span class="cov0" title="0">{ + _ = f.Close() + return "", err + }</span> + } + <span class="cov7" title="3">if err := f.Sync(); err != nil </span><span class="cov0" title="0">{ + _ = f.Close() + return "", err + }</span> + <span class="cov7" title="3">if err := f.Close(); err != nil </span><span class="cov0" title="0">{ + return "", err + }</span> + <span class="cov7" title="3">if err := RunEditor(ed, path); err != nil </span><span class="cov0" title="0">{ + return "", err + }</span> + <span class="cov7" title="3">b, err := os.ReadFile(filepath.Clean(path)) + if err != nil </span><span class="cov0" title="0">{ + return "", err + }</span> + <span class="cov7" title="3">return strings.TrimSpace(string(b)), nil</span> +} +</pre> + + <pre class="file" id="file5" style="display: none">package hexaiaction import ( "context" @@ -1210,7 +1302,7 @@ func echoThrough(infile, outfile string, stdin io.Reader, stdout io.Writer) erro } </pre> - <pre class="file" id="file5" style="display: none">package hexaiaction + <pre class="file" id="file6" style="display: none">package hexaiaction import ( "bufio" @@ -1229,26 +1321,26 @@ import ( // <rest is selection/code> // // If the header is absent, the entire input is treated as selection. -func ParseInput(r io.Reader) (InputParts, error) <span class="cov7" title="4">{ +func ParseInput(r io.Reader) (InputParts, error) <span class="cov7" title="5">{ b, err := io.ReadAll(bufio.NewReader(r)) if err != nil </span><span class="cov0" title="0">{ return InputParts{}, err }</span> - <span class="cov7" title="4">raw := strings.TrimSpace(string(b)) + <span class="cov7" title="5">raw := strings.TrimSpace(string(b)) if raw == "" </span><span class="cov0" title="0">{ return InputParts{Selection: ""}, nil }</span> - <span class="cov7" title="4">lines := strings.Split(raw, "\n") + <span class="cov7" title="5">lines := strings.Split(raw, "\n") // find a case-insensitive line equal to "diagnostics:" diagsIdx := -1 - for i, ln := range lines </span><span class="cov7" title="5">{ + for i, ln := range lines </span><span class="cov8" title="6">{ t := strings.TrimSpace(strings.ToLower(ln)) if t == "diagnostics:" </span><span class="cov1" title="1">{ diagsIdx = i break</span> } } - <span class="cov7" title="4">if diagsIdx < 0 </span><span class="cov5" title="3">{ + <span class="cov7" title="5">if diagsIdx < 0 </span><span class="cov7" title="4">{ return InputParts{Selection: raw}, nil }</span> // collect diagnostics until a blank line or EOF @@ -1281,7 +1373,7 @@ func ExtractInstruction(sel string) (string, string) <span class="cov10" title=" // helpers moved to textutil </pre> - <pre class="file" id="file6" style="display: none">package hexaiaction + <pre class="file" id="file7" style="display: none">package hexaiaction import ( "context" @@ -1294,16 +1386,16 @@ import ( ) // Render performs simple {{var}} replacement like LSP. -func Render(t string, vars map[string]string) string <span class="cov9" title="9">{ return textutil.RenderTemplate(t, vars) }</span> +func Render(t string, vars map[string]string) string <span class="cov9" title="10">{ return textutil.RenderTemplate(t, vars) }</span> // StripFences removes surrounding markdown code fences. -func StripFences(s string) string <span class="cov10" title="10">{ return textutil.StripCodeFences(s) }</span> +func StripFences(s string) string <span class="cov10" title="11">{ return textutil.StripCodeFences(s) }</span> type chatDoer interface { Chat(ctx context.Context, msgs []llm.Message, opts ...llm.RequestOption) (string, error) } -func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruction, selection string) (string, error) <span class="cov5" title="3">{ +func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruction, selection string) (string, error) <span class="cov6" title="4">{ sys := cfg.PromptCodeActionRewriteSystem user := Render(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": instruction, "selection": selection}) return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) @@ -1326,8 +1418,14 @@ func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, dia } func runDocument(ctx context.Context, cfg appconfig.App, client chatDoer, selection string) (string, error) <span class="cov3" title="2">{ - sys := cfg.PromptCodeActionDocumentSystem - user := Render(cfg.PromptCodeActionDocumentUser, map[string]string{"selection": selection}) + sys := cfg.PromptCodeActionDocumentSystem + user := Render(cfg.PromptCodeActionDocumentUser, map[string]string{"selection": selection}) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) +}</span> + +func runSimplify(ctx context.Context, cfg appconfig.App, client chatDoer, selection string) (string, error) <span class="cov0" title="0">{ + sys := cfg.PromptCodeActionSimplifySystem + user := Render(cfg.PromptCodeActionSimplifyUser, map[string]string{"selection": selection}) return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) }</span> @@ -1346,26 +1444,26 @@ func runOnce(ctx context.Context, client chatDoer, sys, user string) (string, er <span class="cov1" title="1">return strings.TrimSpace(StripFences(txt)), nil</span> } -func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) <span class="cov9" title="8">{ +func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) <span class="cov9" title="9">{ msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} txt, err := client.Chat(ctx, msgs, opts...) if err != nil </span><span class="cov0" title="0">{ return "", err }</span> - <span class="cov9" title="8">return strings.TrimSpace(StripFences(txt)), nil</span> + <span class="cov9" title="9">return strings.TrimSpace(StripFences(txt)), nil</span> } // reqOptsFrom builds LLM request options similar to LSP behavior. -func reqOptsFrom(cfg appconfig.App) []llm.RequestOption <span class="cov9" title="8">{ +func reqOptsFrom(cfg appconfig.App) []llm.RequestOption <span class="cov9" title="9">{ opts := []llm.RequestOption{llm.WithMaxTokens(cfg.MaxTokens)} - if cfg.CodingTemperature != nil </span><span class="cov6" title="4">{ + if cfg.CodingTemperature != nil </span><span class="cov7" title="5">{ opts = append(opts, llm.WithTemperature(*cfg.CodingTemperature)) }</span> - <span class="cov9" title="8">return opts</span> + <span class="cov9" title="9">return opts</span> } // Timeout helpers to mirror LSP behavior. -func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov5" title="3">{ +func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov6" title="4">{ return context.WithTimeout(parent, 10*time.Second) }</span> @@ -1374,7 +1472,7 @@ func timeout8s(parent context.Context) (context.Context, context.CancelFunc) <sp }</span> </pre> - <pre class="file" id="file7" style="display: none">package hexaiaction + <pre class="file" id="file8" style="display: none">package hexaiaction import ( "context" @@ -1384,6 +1482,7 @@ import ( "strings" "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/editor" "codeberg.org/snonux/hexai/internal/logging" "codeberg.org/snonux/hexai/internal/llmutils" ) @@ -1393,7 +1492,7 @@ import ( var chooseActionFn = RunTUI var newClientFromApp = llmutils.NewClientFromApp -func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov6" title="3">{ +func Run(ctx context.Conte |
