diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-07 18:00:10 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-07 18:00:10 +0300 |
| commit | f57d63831d604d726685fe31494788e81f17900a (patch) | |
| tree | 5ba8f0af9b47a7a5201b9b3f622e706c5c0cf546 /docs/coverage.html | |
| parent | 3246ebcc5246ed357f45ac32234d5cd34922b9f3 (diff) | |
editor: remove prefilled text in temp files for custom prompts (TUI and CLI)
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.Context, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov7" title="4">{ logger := log.New(stderr, "hexai-tmux-action ", log.LstdFlags|log.Lmsgprefix) cfg := appconfig.Load(logger) client, err := newClientFromApp(cfg) @@ -1401,31 +1500,31 @@ func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error < fmt.Fprintf(stderr, logging.AnsiBase+"hexai-tmux-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) return err }</span> - <span class="cov4" title="2">parts, err := ParseInput(stdin) + <span class="cov6" title="3">parts, err := ParseInput(stdin) if err != nil </span><span class="cov0" title="0">{ fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: failed to read input"+logging.AnsiReset) return err }</span> - <span class="cov4" title="2">if strings.TrimSpace(parts.Selection) == "" </span><span class="cov0" title="0">{ + <span class="cov6" title="3">if strings.TrimSpace(parts.Selection) == "" </span><span class="cov0" title="0">{ return fmt.Errorf("hexai-tmux-action: no input provided on stdin") }</span> - <span class="cov4" title="2">kind, err := chooseActionFn() + <span class="cov6" title="3">kind, err := chooseActionFn() if err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov4" title="2">out, err := executeAction(ctx, kind, parts, cfg, client, stderr) + <span class="cov6" title="3">out, err := executeAction(ctx, kind, parts, cfg, client, stderr) if err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov4" title="2">io.WriteString(stdout, out) + <span class="cov6" title="3">io.WriteString(stdout, out) return nil</span> } -func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) <span class="cov10" title="6">{ - switch kind </span>{ - case ActionSkip:<span class="cov4" title="2"> - return parts.Selection, nil</span> - case ActionRewrite:<span class="cov4" title="2"> +func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) <span class="cov10" title="7">{ + switch kind </span>{ + case ActionSkip:<span class="cov4" title="2"> + return parts.Selection, nil</span> + case ActionRewrite:<span class="cov4" title="2"> instr, cleaned := ExtractInstruction(parts.Selection) if strings.TrimSpace(instr) == "" </span><span class="cov0" title="0">{ fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: no inline instruction found; echoing input"+logging.AnsiReset) @@ -1438,23 +1537,37 @@ func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg a cctx, cancel := timeout10s(ctx) defer cancel() return runDiagnostics(cctx, cfg, client, parts.Diagnostics, parts.Selection)</span> - case ActionDocument:<span class="cov1" title="1"> - cctx, cancel := timeout10s(ctx) - defer cancel() - return runDocument(cctx, cfg, client, parts.Selection)</span> - case ActionGoTest:<span class="cov1" title="1"> - cctx, cancel := timeout8s(ctx) - defer cancel() - return runGoTest(cctx, cfg, client, parts.Selection)</span> - default:<span class="cov0" title="0"> - return parts.Selection, nil</span> - } + case ActionDocument:<span class="cov1" title="1"> + cctx, cancel := timeout10s(ctx) + defer cancel() + return runDocument(cctx, cfg, client, parts.Selection)</span> + case ActionGoTest:<span class="cov1" title="1"> + cctx, cancel := timeout8s(ctx) + defer cancel() + return runGoTest(cctx, cfg, client, parts.Selection)</span> + case ActionSimplify:<span class="cov0" title="0"> + cctx, cancel := timeout10s(ctx) + defer cancel() + return runSimplify(cctx, cfg, client, parts.Selection)</span> + case ActionCustom:<span class="cov1" title="1"> + cctx, cancel := timeout10s(ctx) + defer cancel() + // Open editor for free-form instruction + prompt, err := editor.OpenTempAndEdit(nil) + if err != nil || strings.TrimSpace(prompt) == "" </span><span class="cov0" title="0">{ + fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: custom prompt canceled or empty; echoing input"+logging.AnsiReset) + return parts.Selection, nil + }</span> + <span class="cov1" title="1">return runRewrite(cctx, cfg, client, prompt, parts.Selection)</span> + default:<span class="cov0" title="0"> + return parts.Selection, nil</span> + } } // client construction is shared via internal/llmutils </pre> - <pre class="file" id="file8" style="display: none">package hexaiaction + <pre class="file" id="file9" style="display: none">package hexaiaction import ( "fmt" @@ -1484,12 +1597,14 @@ type model struct { func newModel() model <span class="cov10" title="4">{ items := []list.Item{ item{title: "Rewrite selection", desc: "", kind: ActionRewrite, hotkey: 'r'}, + item{title: "Simplify and improve", desc: "", kind: ActionSimplify, hotkey: 'i'}, item{title: "Document code", desc: "", kind: ActionDocument, hotkey: 'c'}, item{title: "Generate Go unit test(s)", desc: "", kind: ActionGoTest, hotkey: 't'}, + item{title: "Custom prompt", desc: "", kind: ActionCustom, hotkey: 'p'}, item{title: "Skip", desc: "", kind: ActionSkip, hotkey: 's'}, } l := list.New(items, oneLineDelegate{}, 0, 0) - l.Title = "Select Hexai Action" + l.SetShowTitle(false) l.SetShowHelp(false) l.SetShowStatusBar(false) l.SetFilteringEnabled(false) @@ -1533,7 +1648,7 @@ func handleKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) <span class="cov8" m.list.Select(0)</span> case "end":<span class="cov0" title="0"> if n := len(m.list.Items()); n > 0 </span><span class="cov0" title="0">{ m.list.Select(n - 1) }</span> - case "s", "r", "c", "t":<span class="cov1" title="1"> + case "s", "r", "c", "t", "i", "p":<span class="cov1" title="1"> items := m.list.Items() for i := 0; i < len(items); i++ </span><span class="cov1" title="1">{ if it, ok := items[i].(item); ok && strings.ToLower(string(it.hotkey)) == low </span><span class="cov1" title="1">{ @@ -1574,7 +1689,7 @@ func RunTUI() (ActionKind, error) <span class="cov0" title="0">{ } </pre> - <pre class="file" id="file9" style="display: none">package hexaiaction + <pre class="file" id="file10" style="display: none">package hexaiaction import ( "fmt" @@ -1593,8 +1708,8 @@ var ( cursorStyle = lipgloss.NewStyle().Bold(true) ) -func (oneLineDelegate) Height() int <span class="cov8" title="14">{ return 1 }</span> -func (oneLineDelegate) Spacing() int <span class="cov10" title="24">{ return 0 }</span> +func (oneLineDelegate) Height() int <span class="cov8" title="18">{ return 1 }</span> +func (oneLineDelegate) Spacing() int <span class="cov10" title="32">{ return 0 }</span> func (oneLineDelegate) Update(tea.Msg, *list.Model) tea.Cmd <span class="cov1" title="1">{ return nil }</span> func (oneLineDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) <span class="cov2" title="2">{ title := listItem.FilterValue() @@ -1611,7 +1726,7 @@ func (oneLineDelegate) Render(w io.Writer, m list.Model, index int, listItem lis } </pre> - <pre class="file" id="file10" style="display: none">// Summary: Hexai CLI runner; reads input, creates an LLM client, builds messages, + <pre class="file" id="file11" style="display: none">// Summary: Hexai CLI runner; reads input, creates an LLM client, builds messages, // streams or collects the model output, and prints a short summary to stderr. package hexaicli @@ -1626,6 +1741,7 @@ import ( "time" "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/editor" "codeberg.org/snonux/hexai/internal/logging" "codeberg.org/snonux/hexai/internal/llm" "codeberg.org/snonux/hexai/internal/llmutils" @@ -1633,28 +1749,36 @@ import ( // Run executes the Hexai CLI behavior given arguments and I/O streams. // It assumes flags have already been parsed by the caller. -func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov1" title="1">{ - // Load configuration with a logger so file-based config is respected. - logger := log.New(stderr, "hexai ", log.LstdFlags|log.Lmsgprefix) - cfg := appconfig.Load(logger) - client, err := llmutils.NewClientFromApp(cfg) +func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov6" title="3">{ + // Load configuration with a logger so file-based config is respected. + logger := log.New(stderr, "hexai ", log.LstdFlags|log.Lmsgprefix) + cfg := appconfig.Load(logger) + client, err := newClientFromApp(cfg) if err != nil </span><span class="cov1" title="1">{ fmt.Fprintf(stderr, logging.AnsiBase+"hexai: LLM disabled: %v"+logging.AnsiReset+"\n", err) return err }</span> - // Inline the flow here to use configured CLI prompts. - <span class="cov0" title="0">input, rerr := readInput(stdin, args) + // No args: open editor to capture a prompt, then combine with stdin as usual. + <span class="cov4" title="2">if len(args) == 0 </span><span class="cov1" title="1">{ + if prompt, eerr := editor.OpenTempAndEdit(nil); eerr == nil && strings.TrimSpace(prompt) != "" </span><span class="cov1" title="1">{ + args = []string{prompt} + }</span> else <span class="cov0" title="0">{ + // If editor fails or empty, continue; readInput will likely error if no stdin either. + }</span> + } + // Inline the flow here to use configured CLI prompts. + <span class="cov4" title="2">input, rerr := readInput(stdin, args) if rerr != nil </span><span class="cov0" title="0">{ fmt.Fprintln(stderr, logging.AnsiBase+rerr.Error()+logging.AnsiReset) return rerr }</span> - <span class="cov0" title="0">printProviderInfo(stderr, client) + <span class="cov4" title="2">printProviderInfo(stderr, client) msgs := buildMessagesFromConfig(cfg, input) if err := runChat(ctx, client, msgs, input, stdout, stderr); err != nil </span><span class="cov0" title="0">{ fmt.Fprintf(stderr, logging.AnsiBase+"hexai: error: %v"+logging.AnsiReset+"\n", err) return err }</span> - <span class="cov0" title="0">return nil</span> + <span class="cov4" title="2">return nil</span> } // RunWithClient executes the CLI flow using an already-constructed client. @@ -1675,19 +1799,19 @@ func RunWithClient(ctx context.Context, args []string, stdin io.Reader, stdout, } // readInput reads from stdin and args, then combines them per CLI rules. -func readInput(stdin io.Reader, args []string) (string, error) <span class="cov9" title="5">{ +func readInput(stdin io.Reader, args []string) (string, error) <span class="cov10" title="7">{ var stdinData string if fi, err := os.Stdin.Stat(); err == nil && (fi.Mode()&os.ModeCharDevice) == 0 </span><span class="cov7" title="4">{ b, _ := io.ReadAll(bufio.NewReader(stdin)) stdinData = strings.TrimSpace(string(b)) }</span> - <span class="cov9" title="5">argData := strings.TrimSpace(strings.Join(args, " ")) + <span class="cov10" title="7">argData := strings.TrimSpace(strings.Join(args, " ")) switch </span>{ case stdinData != "" && argData != "":<span class="cov1" title="1"> return fmt.Sprintf("%s:\n\n%s", argData, stdinData), nil</span> case stdinData != "":<span class="cov1" title="1"> return stdinData, nil</span> - case argData != "":<span class="cov4" title="2"> + case argData != "":<span class="cov7" title="4"> return argData, nil</span> default:<span class="cov1" title="1"> return "", fmt.Errorf("hexai: no input provided; pass text as an argument or via stdin")</span> @@ -1698,20 +1822,20 @@ func readInput(stdin io.Reader, args []string) (string, error) <span class="cov9 // client construction moved to internal/llmutils // buildMessages creates system and user messages based on input content. -func buildMessages(input string) []llm.Message <span class="cov10" title="6">{ +func buildMessages(input string) []llm.Message <span class="cov9" title="6">{ lower := strings.ToLower(input) 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." if strings.Contains(lower, "explain") </span><span class="cov1" title="1">{ 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." }</span> - <span class="cov10" title="6">return []llm.Message{ + <span class="cov9" title="6">return []llm.Message{ {Role: "system", Content: system}, {Role: "user", Content: input}, }</span> } // buildMessagesFromConfig uses configured CLI system prompts. -func buildMessagesFromConfig(cfg appconfig.App, input string) []llm.Message <span class="cov4" title="2">{ +func buildMessagesFromConfig(cfg appconfig.App, input string) []llm.Message <span class="cov7" title="4">{ lower := strings.ToLower(input) system := cfg.PromptCLIDefaultSystem if strings.Contains(lower, "explain") </span><span class="cov1" title="1">{ @@ -1719,51 +1843,52 @@ func buildMessagesFromConfig(cfg appconfig.App, input string) []llm.Message <spa system = cfg.PromptCLIExplainSystem }</span> } - <span class="cov4" title="2">return []llm.Message{ + <span class="cov7" title="4">return []llm.Message{ {Role: "system", Content: system}, {Role: "user", Content: input}, }</span> } // runChat executes the chat request, handling streaming and summary output. -func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input string, out io.Writer, errw io.Writer) error <span class="cov9" title="5">{ +func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input string, out io.Writer, errw io.Writer) error <span class="cov10" title="7">{ start := time.Now() var output string if s, ok := client.(llm.Streamer); ok </span><span class="cov4" title="2">{ var b strings.Builder - if err := s.ChatStream(ctx, msgs, func(chunk string) </span><span class="cov9" title="5">{ + if err := s.ChatStream(ctx, msgs, func(chunk string) </span><span class="cov8" title="5">{ b.WriteString(chunk) fmt.Fprint(out, chunk) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov4" title="2">output = b.String()</span> - } else<span class="cov6" title="3"> { + } else<span class="cov8" title="5"> { txt, err := client.Chat(ctx, msgs) if err != nil </span><span class="cov4" title="2">{ return err }</span> - <span class="cov1" title="1">output = txt + <span class="cov6" title="3">output = txt fmt.Fprint(out, output)</span> } - <span class="cov6" title="3">dur := time.Since(start) + <span class="cov8" title="5">dur := time.Since(start) fmt.Fprintf(errw, "\n"+logging.AnsiBase+"done provider=%s model=%s time=%s in_bytes=%d out_bytes=%d"+logging.AnsiReset+"\n", client.Name(), client.DefaultModel(), dur.Round(time.Millisecond), len(input), len(output)) return nil</span> } // printProviderInfo writes the provider/model line to stderr. -func printProviderInfo(errw io.Writer, client llm.Client) <span class="cov4" title="2">{ +func printProviderInfo(errw io.Writer, client llm.Client) <span class="cov7" title="4">{ fmt.Fprintf(errw, logging.AnsiBase+"provider=%s model=%s"+logging.AnsiReset+"\n", client.Name(), client.DefaultModel()) }</span> // newClientFromConfig is kept for tests; delegates to llmutils. -func newClientFromConfig(cfg appconfig.App) (llm.Client, error) <span class="cov4" title="2">{ - return llmutils.NewClientFromApp(cfg) -}</span> +var newClientFromApp = llmutils.NewClientFromApp + +// Backcompat for tests referencing the older helper name. +func newClientFromConfig(cfg appconfig.App) (llm.Client, error) <span class="cov4" title="2">{ return newClientFromApp(cfg) }</span> </pre> - <pre class="file" id="file11" style="display: none">// Summary: Hexai LSP runner; configures logging, loads config, builds the LLM client, + <pre class="file" id="file12" style="display: none">// Summary: Hexai LSP runner; configures logging, loads config, builds the LLM client, // and constructs/runs the LSP server (with injectable factory for tests). package hexailsp @@ -1905,11 +2030,13 @@ func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) ls PromptDocumentUser: cfg.PromptCodeActionDocumentUser, PromptGoTestSystem: cfg.PromptCodeActionGoTestSystem, PromptGoTestUser: cfg.PromptCodeActionGoTestUser, + PromptSimplifySystem: cfg.PromptCodeActionSimplifySystem, + PromptSimplifyUser: cfg.PromptCodeActionSimplifyUser, } }</span> </pre> - <pre class="file" id="file12" style="display: none">// Summary: GitHub Copilot client for chat and Codex-style code completion. + <pre class="file" id="file13" style="display: none">// Summary: GitHub Copilot client for chat and Codex-style code completion. package llm import ( @@ -2304,7 +2431,7 @@ func (c copilotClient) CodeCompletion(ctx context.Context, prompt string, suffix // (no streaming decoder needed; we parse whole body lines) </pre> - <pre class="file" id="file13" style="display: none">// Summary: Ollama client against a local server; supports chat responses and streaming via /api/chat. + <pre class="file" id="file14" style="display: none">// Summary: Ollama client against a local server; supports chat responses and streaming via /api/chat. package llm import ( @@ -2522,7 +2649,7 @@ func handleOllamaNon2xx(resp *http.Response, start time.Time) error <span class= } </pre> - <pre class="file" id="file14" style="display: none">// Summary: OpenAI client implementation for chat completions with optional streaming and detailed logging. + <pre class="file" id="file15" style="display: none">// Summary: OpenAI client implementation for chat completions with optional streaming and detailed logging. package llm import ( @@ -2825,7 +2952,7 @@ func parseOpenAIStream(resp *http.Response, start time.Time, onDelta func(string } </pre> - <pre class="file" id="file15" style="display: none">// Summary: LLM provider interfaces, request options, configuration, and factory to build a client from config. + <pre class="file" id="file16" style="display: none">// Summary: LLM provider interfaces, request options, configuration, and factory to build a client from config. package llm import ( @@ -2883,8 +3010,8 @@ type Options struct { type RequestOption func(*Options) func WithModel(model string) RequestOption <span class="cov1" title="1">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Model = model }</span> } -func WithTemperature(t float64) RequestOption <span class="cov5" title="5">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Temperature = t }</span> } -func WithMaxTokens(n int) RequestOption <span class="cov10" title="33">{ return func(o *Options) </span><span class="cov1" title="1">{ o.MaxTokens = n }</span> } +func WithTemperature(t float64) RequestOption <span class="cov5" title="6">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Temperature = t }</span> } +func WithMaxTokens(n int) RequestOption <span class="cov10" title="34">{ return func(o *Options) </span><span class="cov1" title="1">{ o.MaxTokens = n }</span> } func WithStop(stop ...string) RequestOption <span class="cov1" title="1">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Stop = append([]string{}, stop...) }</span> } @@ -2920,11 +3047,11 @@ func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, erro return nil, errors.New("missing OPENAI_API_KEY for provider openai") }</span> // Set coding-friendly default temperature if none provided - <span class="cov6" title="7">if cfg.OpenAITemperature == nil </span><span class="cov5" title="5">{ + <span class="cov5" title="7">if cfg.OpenAITemperature == nil </span><span class="cov5" title="5">{ t := 0.2 cfg.OpenAITemperature = &t }</span> - <span class="cov6" title="7">return newOpenAI(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature), nil</span> + <span class="cov5" title="7">return newOpenAI(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature), nil</span> case "ollama":<span class="cov3" title="3"> if cfg.OllamaTemperature == nil </span><span class="cov2" title="2">{ t := 0.2 @@ -2946,7 +3073,7 @@ func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, erro } </pre> - <pre class="file" id="file16" style="display: none">package llm + <pre class="file" id="file17" style="display: none">package llm import "errors" @@ -2954,7 +3081,7 @@ import "errors" func nilStringErr(msg string) (string, error) <span class="cov10" title="2">{ return "", errors.New(msg) }</span> </pre> - <pre class="file" id="file17" style="display: none">package llmutils + <pre class="file" id="file18" style="display: none">package llmutils import ( "os" @@ -2991,7 +3118,7 @@ func NewClientFromApp(cfg appconfig.App) (llm.Client, error) <span class="cov10" </pre> - <pre class="file" id="file18" style="display: none">package logging + <pre class="file" id="file19" style="display: none">package logging // ChatLogger provides a structured way to log chat interactions. type ChatLogger struct { @@ -3022,7 +3149,7 @@ func (cl ChatLogger) LogStart(stream bool, model string, temp float64, maxTokens } </pre> - <pre class="file" id="file19" style="display: none">// Summary: ANSI-styled logging utilities with a bound standard logger and configurable preview truncation. + <pre class="file" id="file20" style="display: none">// Summary: ANSI-styled logging utilities with a bound standard logger and configurable preview truncation. package logging import ( @@ -3078,7 +3205,7 @@ func PreviewForLog(s string) string <span class="cov7" title="32">{ } </pre> - <pre class="file" id="file20" style="display: none">// Summary: Builds additional context snippets based on configured mode and truncates text by token heuristic. + <pre class="file" id="file21" style="display: none">// Summary: Builds additional context snippets based on configured mode and truncates text by token heuristic. package lsp import ( @@ -3164,7 +3291,7 @@ func truncateToApproxTokens(text string, maxTokens int) string <span class="cov8 } </pre> - <pre class="file" id="file21" style="display: none">// Summary: In-memory document model for the LSP; tracks text, lines, and applies edits. + <pre class="file" id="file22" style="display: none">// Summary: In-memory document model for the LSP; tracks text, lines, and applies edits. package lsp import ( @@ -3311,7 +3438,7 @@ func firstLine(s string) string <span class="cov8" title="25">{ } </pre> - <pre class="file" id="file22" style="display: none">// Summary: LSP JSON-RPC handlers; implements core methods and integrates with the LLM client when enabled. + <pre class="file" id="file23" style="display: none">// Summary: LSP JSON-RPC handlers; implements core methods and integrates with the LLM client when enabled. package lsp import ( @@ -3758,7 +3885,7 @@ func (s *Server) fallbackCompletionItems(docStr string) []CompletionItem <span c }</span> </pre> - <pre class="file" id="file23" style="display: none">// Summary: Code Action handlers and helpers split from handlers.go for clarity. + <pre class="file" id="file24" style="display: none">// Summary: Code Action handlers and helpers split from handlers.go for clarity. package lsp import ( @@ -3791,24 +3918,42 @@ func (s *Server) handleCodeAction(req Request) <span class="cov3" title="3">{ } <span class="cov1" title="1">sel := extractRangeText(d, p.Range) - actions := make([]CodeAction, 0, 4) + actions := make([]CodeAction, 0, 5) if a := s.buildRewriteCodeAction(p, sel); a != nil </span><span class="cov0" title="0">{ actions = append(actions, *a) }</span> <span class="cov1" title="1">if a := s.buildDiagnosticsCodeAction(p, sel); a != nil </span><span class="cov0" title="0">{ actions = append(actions, *a) }</span> - <span class="cov1" title="1">if a := s.buildDocumentCodeAction(p, sel); a != nil </span><span class="cov1" title="1">{ - actions = append(actions, *a) - }</span> - <span class="cov1" title="1">if a := s.buildGoUnitTestCodeAction(p); a != nil </span><span class="cov1" title="1">{ - actions = append(actions, *a) - }</span> + <span class="cov1" title="1">if a := s.buildDocumentCodeAction(p, sel); a != nil </span><span class="cov1" title="1">{ + actions = append(actions, *a) + }</span> + <span class="cov1" title="1">if a := s.buildGoUnitTestCodeAction(p); a != nil </span><span class="cov1" title="1">{ + actions = append(actions, *a) + }</span> + <span class="cov1" title="1">if a := s.buildSimplifyCodeAction(p, sel); a != nil </span><span class="cov1" title="1">{ + actions = append(actions, *a) + }</span> <span class="cov1" title="1">if len(req.ID) != 0 </span><span class="cov1" title="1">{ s.reply(req.ID, actions, nil) }</span> } +func (s *Server) buildSimplifyCodeAction(p CodeActionParams, sel string) *CodeAction <span class="cov1" title="1">{ + if strings.TrimSpace(sel) == "" </span><span class="cov0" title="0">{ + return nil + }</span> + <span class="cov1" title="1">payload := struct { + Type string `json:"type"` + URI string `json:"uri"` + Range Range `json:"range"` + Selection string `json:"selection"` + }{Type: "simplify", URI: p.TextDocument.URI, Range: p.Range, Selection: sel} + raw, _ := json.Marshal(payload) + ca := CodeAction{Title: "Hexai: simplify and improve", Kind: "refactor", Data: raw} + return &ca</span> +} + func (s *Server) buildRewriteCodeAction(p CodeActionParams, sel string) *CodeAction <span class="cov3" title="3">{ if instr, cleaned := instructionFromSelection(sel); strings.TrimSpace(instr) != "" </span><span class="cov1" title="1">{ payload := struct { @@ -3857,7 +4002,7 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class if err := json.Unmarshal(ca.Data, &payload); err != nil </span><span class="cov0" title="0">{ return ca, false }</span> - <span class="cov5" title="12">switch payload.Type </span>{ + <span class="cov5" title="12">switch payload.Type </span>{ case "rewrite":<span class="cov3" title="4"> sys := s.promptRewriteSystem user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": payload.Instruction, "selection": payload.Selection}) @@ -3915,17 +4060,34 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class } else<span class="cov0" title="0"> { logging.Logf("lsp ", "codeAction document llm error: %v", err) }</span> - case "go_test":<span class="cov0" title="0"> - if edit, jumpURI, jumpRange, ok := s.resolveGoTest(payload.URI, payload.Range.Start); ok </span><span class="cov0" title="0">{ - ca.Edit = &edit - // After edit is applied, ask client to jump to new test function - ca.Command = &Command{Title: "Jump to generated test", Command: "hexai.showDocument", Arguments: []any{jumpURI, jumpRange}} - // Also send a server-initiated showDocument shortly after resolve to cover - // clients that do not execute commands from code actions. - s.deferShowDocument(jumpURI, jumpRange) - return ca, true - }</span> - } + case "go_test":<span class="cov0" title="0"> + if edit, jumpURI, jumpRange, ok := s.resolveGoTest(payload.URI, payload.Range.Start); ok </span><span class="cov0" title="0">{ + ca.Edit = &edit + // After edit is applied, ask client to jump to new test function + ca.Command = &Command{Title: "Jump to generated test", Command: "hexai.showDocument", Arguments: []any{jumpURI, jumpRange}} + // Also send a server-initiated showDocument shortly after resolve to cover + // clients that do not execute commands from code actions. + s.deferShowDocument(jumpURI, jumpRange) + return ca, true + }</span> + case "simplify":<span class="cov0" title="0"> + sys := s.promptRewriteSystem + // Reuse rewrite user template with a fixed instruction + user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": "Simplify and improve the code while preserving behavior. Return only the improved code.", "selection": payload.Selection}) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + opts := s.llmRequestOpts() + if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil </span><span class="cov0" title="0">{ + if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov0" title="0">{ + edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} + ca.Edit = &edit + return ca, true + }</span> + } else<span class="cov0" title="0"> { + logging.Logf("lsp ", "codeAction simplify llm error: %v", err) + }</span> + } <span class="cov0" title="0">return ca, false</span> } @@ -4285,7 +4447,7 @@ func exportName(name string) string <span class="cov2" title="2">{ } </pre> - <pre class="file" id="file24" style="display: none">// Summary: Completion handlers split from handlers.go to reduce file size and isolate feature logic. + <pre class="file" id="file25" style="display: none">// Summary: Completion handlers split from handlers.go to reduce file size and isolate feature logic. package lsp import ( @@ -4680,7 +4842,7 @@ func (s *Server) postProcessCompletion(text string, leftOfCursor string, current } </pre> - <pre class="file" id="file25" style="display: none">// Summary: Document open/change/close and in-editor chat handlers split out of handlers.go. + <pre class="file" id="file26" style="display: none">// Summary: Document open/change/close and in-editor chat handlers split out of handlers.go. package lsp import ( @@ -5009,7 +5171,7 @@ func (s *Server) deferShowDocument(uri string, sel Range) <span class="cov1" tit } </pre> - <pre class="file" id="file26" style="display: none">// Summary: ExecuteCommand handler to support post-edit navigation (jump to generated test). + <pre class="file" id="file27" style="display: none">// Summary: ExecuteCommand handler to support post-edit navigation (jump to generated test). package lsp import ( @@ -5045,7 +5207,7 @@ func (s *Server) handleExecuteCommand(req Request) <span class="cov8" title="1"> } </pre> - <pre class="file" id="file27" style="display: none">// Summary: Initialization and lifecycle handlers split from handlers.go. + <pre class="file" id="file28" style="display: none">// Summary: Initialization and lifecycle handlers split from handlers.go. package lsp import ( @@ -5088,7 +5250,7 @@ func (s *Server) handleExit() <span class="cov0" title="0">{ }</span> </pre> - <pre class="file" id="file28" style="display: none">// Summary: Generic LSP helpers shared across handlers (LLM opts, prompts, text utils, counters). + <pre class="file" id="file29" style="display: none">// Summary: Generic LSP helpers shared across handlers (LLM opts, prompts, text utils, counters). package lsp import ( @@ -5551,7 +5713,7 @@ func collectSemicolonMarkers(line string, lineNum int) []TextEdit <span class="c } </pre> - <pre class="file" id="file29" style="display: none">// Summary: Minimal LSP server over stdio; manages documents, dispatches requests, and tracks stats. + <pre class="file" id="file30" style="display: none">// Summary: Minimal LSP server over stdio; manages documents, dispatches requests, and tracks stats. package lsp import ( @@ -5631,9 +5793,11 @@ type Server struct { promptDocumentSystem string promptRewriteUser string promptDiagnosticsUser string - promptDocumentUser string - promptGoTestSystem string - promptGoTestUser string + promptDocumentUser string + promptGoTestSystem string + promptGoTestUser string + promptSimplifySystem string + promptSimplifyUser string } // ServerOptions collects configuration for NewServer to avoid long parameter lists. @@ -5672,8 +5836,10 @@ type ServerOptions struct { PromptRewriteUser string PromptDiagnosticsUser string PromptDocumentUser string - PromptGoTestSystem string - PromptGoTestUser string + PromptGoTestSystem string + PromptGoTestUser string + PromptSimplifySystem string + PromptSimplifyUser string } func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) *Server <span class="cov10" title="7">{ @@ -5752,9 +5918,11 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) s.promptDocumentSystem = opts.PromptDocumentSystem s.promptRewriteUser = opts.PromptRewriteUser s.promptDiagnosticsUser = opts.PromptDiagnosticsUser - s.promptDocumentUser = opts.PromptDocumentUser - s.promptGoTestSystem = opts.PromptGoTestSystem - s.promptGoTestUser = opts.PromptGoTestUser + s.promptDocumentUser = opts.PromptDocumentUser + s.promptGoTestSystem = opts.PromptGoTestSystem + s.promptGoTestUser = opts.PromptGoTestUser + s.promptSimplifySystem = opts.PromptSimplifySystem + s.promptSimplifyUser = opts.PromptSimplifyUser // Assign package-level inline trigger chars for free helper functions if s.inlineOpen != "" </span><span class="cov10" title="7">{ @@ -5812,7 +5980,7 @@ func (s *Server) Run() error <span class="cov1" title="1">{ } </pre> - <pre class="file" id="file30" style="display: none">// Summary: LSP transport utilities to read and write JSON-RPC messages with Content-Length framing. + <pre class="file" id="file31" style="display: none">// Summary: LSP transport utilities to read and write JSON-RPC messages with Content-Length framing. package lsp import ( @@ -5880,7 +6048,7 @@ func (s *Server) writeMessage(v any) <span class="cov10" title="18">{ } </pre> - <pre class="file" id="file31" style="display: none">package testutil + <pre class="file" id="file32" style="display: none">package testutil // MultilineDocBlock returns a realistic multi-line documentation block. func MultilineDocBlock() string <span class="cov8" title="1">{ @@ -5908,47 +6076,47 @@ func MalformedJSON() string <span class="cov8" title="1">{ }</span> </pre> - <pre class="file" id="file32" style="display: none">package textutil + <pre class="file" id="file33" style="display: none">package textutil import "strings" // RenderTemplate performs simple {{var}} replacement in a template string. -func RenderTemplate(t string, vars map[string]string) string <span class="cov8" title="45">{ +func RenderTemplate(t string, vars map[string]string) string <span class="cov8" title="46">{ if t == "" || len(vars) == 0 </span><span class="cov5" title="11">{ return t }</span> - <span class="cov7" title="34">out := t - for k, v := range vars </span><span class="cov9" title="93">{ + <span class="cov7" title="35">out := t + for k, v := range vars </span><span class="cov9" title="95">{ out = strings.ReplaceAll(out, "{{"+k+"}}", v) }</span> - <span class="cov7" title="34">return out</span> + <span class="cov7" title="35">return out</span> } // StripCodeFences removes surrounding Markdown triple-backtick fences. -func StripCodeFences(s string) string <span class="cov8" title="51">{ +func StripCodeFences(s string) string <span class="cov8" title="52">{ t := strings.TrimSpace(s) if t == "" </span><span class="cov0" title="0">{ return t }</span> - <span class="cov8" title="51">lines := strings.Split(t, "\n") + <span class="cov8" title="52">lines := strings.Split(t, "\n") start := 0 for start < len(lines) && strings.TrimSpace(lines[start]) == "" </span><span class="cov0" title="0">{ start++ }</span> - <span class="cov8" title="51">end := len(lines) - 1 + <span class="cov8" title="52">end := len(lines) - 1 for end >= 0 && strings.TrimSpace(lines[end]) == "" </span><span class="cov0" title="0">{ end-- }</span> - <span class="cov8" title="51">if start >= len(lines) || end < 0 || start > end </span><span class="cov0" title="0">{ + <span class="cov8" title="52">if start >= len(lines) || end < 0 || start > end </span><span class="cov0" title="0">{ return t }</span> - <span class="cov8" title="51">first := strings.TrimSpace(lines[start]) + <span class="cov8" title="52">first := strings.TrimSpace(lines[start]) last := strings.TrimSpace(lines[end]) if strings.HasPrefix(first, "```") && last == "```" && end > start </span><span class="cov6" title="20">{ inner := strings.Join(lines[start+1:end], "\n") return inner }</span> - <span class="cov7" title="31">return t</span> + <span class="cov7" title="32">return t</span> } // InstructionFromSelection extracts the first inline instruction and returns @@ -6024,7 +6192,7 @@ func FindStrictInlineTag(line string) (text string, left, right int, ok bool) <s </pre> - <pre class="file" id="file33" style="display: none">package tmux + <pre class="file" id="file34" style="display: none">package tmux import ( "os" |
