summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-07 11:26:10 +0300
committerPaul Buetow <paul@buetow.org>2025-09-07 11:26:10 +0300
commit8889949ad3851bfbf36ff5b73128286d67c88201 (patch)
tree0f515ae6ee3da898dea113799c09e943f3e3f8fb /docs
parent7c0266e94378f6121719939c6d53915eb72eed3e (diff)
tiding up
Diffstat (limited to 'docs')
-rw-r--r--docs/configuration.md33
-rw-r--r--docs/coverage.html513
-rw-r--r--docs/coverage.out6525
-rw-r--r--docs/usage.md8
4 files changed, 6103 insertions, 976 deletions
diff --git a/docs/configuration.md b/docs/configuration.md
index 3fbb1dc..dc4adbd 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -30,23 +30,30 @@ API keys:
Selecting a provider
- Sectioned: set `[provider] name = "openai" | "copilot" | "ollama"`.
-- Flat: set `provider = "openai" | "copilot" | "ollama"`.
- If omitted, Hexai defaults to `openai`.
-Provider-specific options
-
-- See [config.toml.example](../config.toml.example) for the per-provider tables and defaults.
-
-Notes:
+Notes on Ollama:
- Ensure the model is available locally (e.g., `ollama pull qwen3-coder:30b-a3b-q4_K_M`).
- Alternatively, run Ollama in OpenAI‑compatible mode and use the OpenAI provider with
`openai_base_url` pointed at your local endpoint.
-LSP completion tuning
-
-- See the [completion] section in [config.toml.example](../config.toml.example).
-
-Temperature behavior
-
-- Defaults and recommended ranges are commented inline in [config.toml.example](../config.toml.example) under [general] and provider tables.
+Hexai Action (TUI) configuration
+
+This is mostly useful when Helix runs in a [tmux](https://tmux.github.io/) session!
+
+- Helix integration (recommended): bind a key to pipe the current selection to `hexai-action` and replace it with the output.
+ - Example: `C-a = ":pipe hexai-action"`
+- Default behavior:
+ - Inline TUI when run in a real terminal (TTY).
+ - When invoked via Helix `:pipe` and a tmux session is available, `hexai-action` opens a split pane to render the menu and returns the result on stdout for Helix to apply.
+ - If no TTY and no tmux are available, it falls back to echoing the input.
+- Flags:
+ - `--infile` Read input from the given file instead of stdin.
+ - `--outfile` Write output to the given file instead of stdout (truncates/creates).
+ - `--tmux` force tmux-pane mode.
+ - `--no-tmux` disable tmux mode even if available.
+ - `--tmux-target` tmux target pane/window (advanced).
+ - `--tmux-split v|h` split orientation (default: `v`).
+ - `--tmux-percent N` split size percentage (default: `33`).
+ - `--ui-child` internal; used by the parent process when spawning inside tmux.
diff --git a/docs/coverage.html b/docs/coverage.html
index 6b80630..2003a0d 100644
--- a/docs/coverage.html
+++ b/docs/coverage.html
@@ -59,19 +59,19 @@
<option value="file1">codeberg.org/snonux/hexai/cmd/hexai/main.go (71.4%)</option>
- <option value="file2">codeberg.org/snonux/hexai/cmd/internal/hexai-action/main.go (0.0%)</option>
+ <option value="file2">codeberg.org/snonux/hexai/cmd/internal/hexai-action/main.go (69.3%)</option>
<option value="file3">codeberg.org/snonux/hexai/internal/appconfig/config.go (91.6%)</option>
<option value="file4">codeberg.org/snonux/hexai/internal/hexaiaction/parse.go (92.6%)</option>
- <option value="file5">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (81.1%)</option>
+ <option value="file5">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (91.9%)</option>
- <option value="file6">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (33.3%)</option>
+ <option value="file6">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (48.7%)</option>
- <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (47.3%)</option>
+ <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (65.5%)</option>
- <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (91.7%)</option>
+ <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (100.0%)</option>
<option value="file9">codeberg.org/snonux/hexai/internal/hexaicli/run.go (78.8%)</option>
@@ -119,6 +119,8 @@
<option value="file31">codeberg.org/snonux/hexai/internal/textutil/textutil.go (89.0%)</option>
+ <option value="file32">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option>
+
</select>
</div>
<div id="legend">
@@ -198,18 +200,240 @@ func main() <span class="cov8" title="1">{
import (
"context"
+ "flag"
"fmt"
+ "io"
"os"
+ "path/filepath"
+ "time"
"codeberg.org/snonux/hexai/internal/hexaiaction"
+ "codeberg.org/snonux/hexai/internal/tmux"
+ "golang.org/x/term"
)
func main() <span class="cov0" title="0">{
- if err := hexaiaction.Run(context.Background(), os.Stdin, os.Stdout, os.Stderr); err != nil </span><span class="cov0" title="0">{
+ infile := flag.String("infile", "", "Read input from this file instead of stdin")
+ outfile := flag.String("outfile", "", "Write output to this file instead of stdout")
+ // Tmux/UI flags
+ forceTmux := flag.Bool("tmux", false, "Force running the UI in a tmux split-pane (auto if not set)")
+ noTmux := flag.Bool("no-tmux", false, "Disable tmux mode even if available")
+ uiChild := flag.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically")
+ tmuxTarget := flag.String("tmux-target", "", "tmux split target (advanced)")
+ tmuxSplit := flag.String("tmux-split", "v", "tmux split orientation: v or h")
+ tmuxPercent := flag.Int("tmux-percent", 33, "tmux split size percentage (1-100)")
+ flag.Parse()
+
+ // Child mode: run TUI and write atomically to -outfile
+ if *uiChild </span><span class="cov0" title="0">{
+ if err := runChild(*infile, *outfile); err != nil </span><span class="cov0" title="0">{
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }</span>
+ <span class="cov0" title="0">return</span>
+ }
+
+ // Parent mode: decide inline vs tmux
+ <span class="cov0" title="0">if shouldRunInTmux(*forceTmux, *noTmux) </span><span class="cov0" title="0">{
+ if err := runInTmuxParent(*tmuxTarget, *tmuxSplit, *tmuxPercent); err != nil </span><span class="cov0" title="0">{
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }</span>
+ <span class="cov0" title="0">return</span>
+ }
+
+ // Inline path: only if we have a TTY for UI; otherwise echo input
+ <span class="cov0" title="0">if isTTY(os.Stdout.Fd()) &amp;&amp; isTTY(os.Stdin.Fd()) </span><span class="cov0" title="0">{
+ in, out, closeIn, closeOut, err := openIO(*infile, *outfile)
+ if err != nil </span><span class="cov0" title="0">{
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }</span>
+ <span class="cov0" title="0">defer closeIn()
+ defer closeOut()
+ if err := hexaiactionRun(context.Background(), in, out, os.Stderr); err != nil </span><span class="cov0" title="0">{
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }</span>
+ <span class="cov0" title="0">return</span>
+ }
+
+ // Fallback: no TTY and tmux not available; echo input to output
+ <span class="cov0" title="0">if err := echoThrough(*infile, *outfile); err != nil </span><span class="cov0" title="0">{
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}</span>
}
+
+// openIO returns readers/writers for infile/outfile flags with deferred closers.
+func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error) <span class="cov10" title="6">{
+ in := io.Reader(os.Stdin)
+ out := io.Writer(os.Stdout)
+ closeIn := func() </span>{<span class="cov0" title="0">}</span>
+ <span class="cov10" title="6">closeOut := func() </span>{<span class="cov1" title="1">}</span>
+
+ <span class="cov10" title="6">if path := infile; path != "" </span><span class="cov9" title="5">{
+ f, err := os.Open(path)
+ if err != nil </span><span class="cov1" title="1">{
+ return nil, nil, func() </span>{<span class="cov0" title="0">}</span>, func() {<span class="cov0" title="0">}</span>, fmt.Errorf("hexai-action: cannot open infile: %w", err)
+ }
+ <span class="cov7" title="4">in = f
+ closeIn = func() </span><span class="cov7" title="4">{ _ = f.Close() }</span>
+ }
+ <span class="cov9" title="5">if path := outfile; path != "" </span><span class="cov7" title="4">{
+ f, err := os.Create(path)
+ if err != nil </span><span class="cov1" title="1">{
+ return nil, nil, func() </span>{<span class="cov0" title="0">}</span>, func() {<span class="cov0" title="0">}</span>, fmt.Errorf("hexai-action: cannot open outfile: %w", err)
+ }
+ <span class="cov6" title="3">out = f
+ closeOut = func() </span><span class="cov6" title="3">{ _ = f.Close() }</span>
+ }
+ <span class="cov7" title="4">return in, out, closeIn, closeOut, nil</span>
+}
+
+// runChild runs the interactive flow and writes the final output atomically to outfile.
+var hexaiactionRun = hexaiaction.Run
+
+func runChild(infile, outfile string) error <span class="cov6" title="3">{
+ if outfile == "" </span><span class="cov1" title="1">{
+ // No atomic handoff needed; just run normally to stdout
+ in, out, closeIn, closeOut, err := openIO(infile, "")
+ if err != nil </span><span class="cov0" title="0">{
+ return err
+ }</span>
+ <span class="cov1" title="1">defer closeIn()
+ defer closeOut()
+ return hexaiactionRun(context.Background(), in, out, os.Stderr)</span>
+ }
+ <span class="cov4" title="2">tmp := outfile + ".tmp"
+ in, out, closeIn, closeOut, err := openIO(infile, tmp)
+ if err != nil </span><span class="cov0" title="0">{
+ return err
+ }</span>
+ <span class="cov4" title="2">defer closeIn()
+ if err := hexaiactionRun(context.Background(), in, out, os.Stderr); err != nil </span><span class="cov1" title="1">{
+ // On error, try to echo input to tmp to avoid blocking
+ closeOut()
+ if copyErr := echoThrough(infile, tmp); copyErr != nil </span><span class="cov0" title="0">{
+ return fmt.Errorf("hexai-action child: %v; echo failed: %v", err, copyErr)
+ }</span>
+ } else<span class="cov1" title="1"> {
+ closeOut()
+ }</span>
+ <span class="cov4" title="2">return os.Rename(tmp, outfile)</span>
+}
+
+var isTTYFn = isTTY
+var tmuxAvailableFn = tmux.Available
+var splitRunFn = tmux.SplitRun
+var osExecutableFn = os.Executable
+
+func shouldRunInTmux(forceTmux, noTmux bool) bool <span class="cov9" title="5">{
+ if noTmux </span><span class="cov1" title="1">{
+ return false
+ }</span>
+ <span class="cov7" title="4">if forceTmux </span><span class="cov1" title="1">{
+ return true
+ }</span>
+ // Auto: prefer tmux when stdio are not TTYs (Helix :pipe scenario)
+ <span class="cov6" title="3">if !(isTTYFn(os.Stdin.Fd()) &amp;&amp; isTTYFn(os.Stdout.Fd())) &amp;&amp; tmuxAvailableFn() </span><span class="cov1" title="1">{
+ return true
+ }</span>
+ <span class="cov4" title="2">return false</span>
+}
+
+func isTTY(fd uintptr) bool <span class="cov0" title="0">{ return term.IsTerminal(int(fd)) }</span>
+
+func runInTmuxParent(target, split string, percent int) error <span class="cov6" title="3">{
+ // Prepare temp files
+ dir, err := os.MkdirTemp("", "hexai-action-")
+ if err != nil </span><span class="cov0" title="0">{
+ return err
+ }</span>
+ <span class="cov6" title="3">defer func() </span><span class="cov6" title="3">{ _ = os.RemoveAll(dir) }</span>()
+ <span class="cov6" title="3">inPath := filepath.Join(dir, "input.txt")
+ outPath := filepath.Join(dir, "reply.txt")
+ // Read stdin and persist to inPath
+ if err := persistStdin(inPath); err != nil </span><span class="cov0" title="0">{
+ return err
+ }</span>
+ // Build child argv
+ <span class="cov6" title="3">exe, err := osExecutableFn()
+ if err != nil </span><span class="cov1" title="1">{
+ return err
+ }</span>
+ <span class="cov4" title="2">argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath}
+ // Spawn tmux split
+ opts := tmux.SplitOpts{Target: target, Vertical: split != "h", Percent: percent}
+ if err := splitRunFn(opts, argv); err != nil </span><span class="cov1" title="1">{
+ return err
+ }</span>
+ // Wait for outfile to appear
+ <span class="cov1" title="1">if err := waitForFile(outPath, 60*time.Second); err != nil </span><span class="cov0" title="0">{
+ return err
+ }</span>
+ // Print to stdout
+ <span class="cov1" title="1">return catFileToStdout(outPath)</span>
+}
+
+func persistStdin(path string) error <span class="cov9" title="5">{
+ f, err := os.Create(path)
+ if err != nil </span><span class="cov1" title="1">{
+ return err
+ }</span>
+ <span class="cov7" title="4">defer func() </span><span class="cov7" title="4">{ _ = f.Close() }</span>()
+ <span class="cov7" title="4">if _, err := io.Copy(f, os.Stdin); err != nil </span><span class="cov0" title="0">{
+ return err
+ }</span>
+ <span class="cov7" title="4">return f.Sync()</span>
+}
+
+func waitForFile(path string, timeout time.Duration) error <span class="cov6" title="3">{
+ deadline := time.Now().Add(timeout)
+ for </span><span class="cov9" title="5">{
+ if _, err := os.Stat(path); err == nil </span><span class="cov4" title="2">{
+ return nil
+ }</span>
+ <span class="cov6" title="3">if time.Now().After(deadline) </span><span class="cov1" title="1">{
+ return fmt.Errorf("hexai-action: timeout waiting for reply file")
+ }</span>
+ <span class="cov4" title="2">time.Sleep(200 * time.Millisecond)</span>
+ }
+}
+
+func catFileToStdout(path string) error <span class="cov6" title="3">{
+ f, err := os.Open(path)
+ if err != nil </span><span class="cov1" title="1">{
+ return err
+ }</span>
+ <span class="cov4" title="2">defer func() </span><span class="cov4" title="2">{ _ = f.Close() }</span>()
+ <span class="cov4" title="2">_, err = io.Copy(os.Stdout, f)
+ return err</span>
+}
+
+func echoThrough(infile, outfile string) error <span class="cov7" title="4">{
+ // Read from infile or stdin and write to outfile or stdout
+ var in io.Reader = os.Stdin
+ var out io.Writer = os.Stdout
+ if infile != "" </span><span class="cov6" title="3">{
+ f, err := os.Open(infile)
+ if err != nil </span><span class="cov0" title="0">{
+ return err
+ }</span>
+ <span class="cov6" title="3">defer func() </span><span class="cov6" title="3">{ _ = f.Close() }</span>()
+ <span class="cov6" title="3">in = f</span>
+ }
+ <span class="cov7" title="4">if outfile != "" </span><span class="cov6" title="3">{
+ f, err := os.Create(outfile)
+ if err != nil </span><span class="cov1" title="1">{
+ return err
+ }</span>
+ <span class="cov4" title="2">defer func() </span><span class="cov4" title="2">{ _ = f.Close() }</span>()
+ <span class="cov4" title="2">out = f</span>
+ }
+ <span class="cov6" title="3">_, err := io.Copy(out, in)
+ return err</span>
+}
</pre>
<pre class="file" id="file3" style="display: none">// Summary: Application configuration model and loader; reads ~/.config/hexai/config.toml and merges defaults.
@@ -300,7 +524,7 @@ type App struct {
}
// Constructor: defaults for App (kept first among functions)
-func newDefaultConfig() App <span class="cov5" title="13">{
+func newDefaultConfig() App <span class="cov5" title="14">{
// Coding-friendly default temperature across providers
// Users can override per provider in config.toml (including 0.0).
t := 0.2
@@ -351,17 +575,17 @@ func newDefaultConfig() App <span class="cov5" title="13">{
// 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="12">{
+func Load(logger *log.Logger) App <span class="cov5" title="13">{
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="8">configPath, err := getConfigPath()
+ <span class="cov4" title="9">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="8"> {
+ }</span> else<span class="cov4" title="9"> {
if fileCfg, err := loadFromFile(configPath, logger); err == nil &amp;&amp; fileCfg != nil </span><span class="cov3" title="4">{
cfg.mergeWith(fileCfg)
}</span>
@@ -370,10 +594,10 @@ func Load(logger *log.Logger) App <span class="cov5" title="12">{
}
// Environment overrides (take precedence over file)
- <span class="cov4" title="8">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{
cfg.mergeWith(envCfg)
}</span>
- <span class="cov4" title="8">return cfg</span>
+ <span class="cov4" title="9">return cfg</span>
}
// Private helpers
@@ -644,13 +868,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="cov4" title="9">{
+func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="10">{
b, err := os.ReadFile(path)
- if err != nil </span><span class="cov2" title="3">{
+ if err != nil </span><span class="cov3" title="4">{
if !os.IsNotExist(err) &amp;&amp; logger != nil </span><span class="cov0" title="0">{
logger.Printf("cannot open TOML config file %s: %v", path, err)
}</span>
- <span class="cov2" title="3">return nil, err</span>
+ <span class="cov3" title="4">return nil, err</span>
}
<span class="cov4" title="6">var tables fileConfig
@@ -868,33 +1092,33 @@ func (a *App) mergeProviderFields(other *App) <span class="cov5" title="14">{
}</span>
}
-func getConfigPath() (string, error) <span class="cov4" title="9">{
+func getConfigPath() (string, error) <span class="cov4" title="10">{
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="cov2" title="2"> {
+ }</span> else<span class="cov2" title="3"> {
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="cov2" title="2">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span>
+ <span class="cov2" title="3">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span>
}
- <span class="cov4" title="9">return configPath, nil</span>
+ <span class="cov4" title="10">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="8">{
+func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="9">{
var out App
var any bool
// helpers
- getenv := func(k string) string </span><span class="cov10" title="192">{ return strings.TrimSpace(os.Getenv(k)) }</span>
- <span class="cov4" title="8">parseInt := func(k string) (int, bool) </span><span class="cov7" title="56">{
+ getenv := func(k string) string </span><span class="cov10" title="216">{ return strings.TrimSpace(os.Getenv(k)) }</span>
+ <span class="cov4" title="9">parseInt := func(k string) (int, bool) </span><span class="cov7" title="63">{
v := getenv(k)
- if v == "" </span><span class="cov7" title="49">{
+ if v == "" </span><span class="cov7" title="56">{
return 0, false
}</span>
<span class="cov4" title="7">n, err := strconv.Atoi(v)
@@ -906,9 +1130,9 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="8">{
}
<span class="cov4" title="7">return n, true</span>
}
- <span class="cov4" title="8">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov6" title="32">{
+ <span class="cov4" title="9">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="36">{
v := getenv(k)
- if v == "" </span><span class="cov6" title="28">{
+ if v == "" </span><span class="cov6" title="32">{
return nil, false
}</span>
<span class="cov3" title="4">f, err := strconv.ParseFloat(v, 64)
@@ -921,43 +1145,43 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="8">{
<span class="cov3" title="4">return &amp;f, true</span>
}
- <span class="cov4" title="8">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{
out.MaxTokens = n
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{
out.ContextMode = s
any = true
}</span>
- <span class="cov4" title="8">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">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="8">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">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="8">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">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="8">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">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="8">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">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="8">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">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="8">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{
out.CodingTemperature = f
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">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">{
@@ -967,19 +1191,19 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="8">{
}
<span class="cov1" title="1">any = true</span>
}
- <span class="cov4" title="8">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{
out.InlineOpen = s
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{
out.InlineClose = s
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{
out.ChatSuffix = s
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{
+ <span class="cov4" title="9">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">{
@@ -989,52 +1213,52 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="8">{
}
<span class="cov0" title="0">any = true</span>
}
- <span class="cov4" title="8">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{
out.Provider = s
any = true
}</span>
// Provider-specific
- <span class="cov4" title="8">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{
out.OpenAIBaseURL = s
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{
out.OpenAIModel = s
any = true
}</span>
- <span class="cov4" title="8">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{
out.OpenAITemperature = f
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{
out.OllamaBaseURL = s
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{
out.OllamaModel = s
any = true
}</span>
- <span class="cov4" title="8">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{
out.OllamaTemperature = f
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{
out.CopilotBaseURL = s
any = true
}</span>
- <span class="cov4" title="8">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{
out.CopilotModel = s
any = true
}</span>
- <span class="cov4" title="8">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <span class="cov4" title="9">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{
out.CopilotTemperature = f
any = true
}</span>
- <span class="cov4" title="8">if !any </span><span class="cov4" title="7">{
+ <span class="cov4" title="9">if !any </span><span class="cov4" title="8">{
return nil
}</span>
<span class="cov1" title="1">return &amp;out</span>
@@ -1125,16 +1349,16 @@ import (
)
// Render performs simple {{var}} replacement like LSP.
-func Render(t string, vars map[string]string) string <span class="cov10" title="8">{ return textutil.RenderTemplate(t, vars) }</span>
+func Render(t string, vars map[string]string) string <span class="cov9" title="8">{ return textutil.RenderTemplate(t, vars) }</span>
// StripFences removes surrounding markdown code fences.
-func StripFences(s string) string <span class="cov10" title="8">{ return textutil.StripCodeFences(s) }</span>
+func StripFences(s string) string <span class="cov10" title="9">{ 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="cov4" title="2">{
+func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruction, selection string) (string, error) <span class="cov3" title="2">{
sys := cfg.PromptCodeActionRewriteSystem
user := Render(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": instruction, "selection": selection})
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))
@@ -1142,11 +1366,11 @@ func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruc
func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, diags []string, selection string) (string, error) <span class="cov1" title="1">{
var b strings.Builder
- for i, d := range diags </span><span class="cov4" title="2">{
+ for i, d := range diags </span><span class="cov3" title="2">{
if strings.TrimSpace(d) == "" </span><span class="cov0" title="0">{
continue</span>
}
- <span class="cov4" title="2">b.WriteString(strings.TrimSpace(d))
+ <span class="cov3" title="2">b.WriteString(strings.TrimSpace(d))
if i &lt; len(diags)-1 </span><span class="cov1" title="1">{
b.WriteString("\n")
}</span>
@@ -1156,47 +1380,47 @@ func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, dia
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))</span>
}
-func runDocument(ctx context.Context, cfg appconfig.App, client chatDoer, selection string) (string, error) <span class="cov4" title="2">{
+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})
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))
}</span>
-func runGoTest(ctx context.Context, cfg appconfig.App, client chatDoer, funcCode string) (string, error) <span class="cov4" title="2">{
+func runGoTest(ctx context.Context, cfg appconfig.App, client chatDoer, funcCode string) (string, error) <span class="cov3" title="2">{
sys := cfg.PromptCodeActionGoTestSystem
user := Render(cfg.PromptCodeActionGoTestUser, map[string]string{"function": funcCode})
return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))
}</span>
-func runOnce(ctx context.Context, client chatDoer, sys, user string) (string, error) <span class="cov0" title="0">{
+func runOnce(ctx context.Context, client chatDoer, sys, user string) (string, error) <span class="cov1" title="1">{
msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}}
txt, err := client.Chat(ctx, msgs)
if err != nil </span><span class="cov0" title="0">{
return "", err
}</span>
- <span class="cov0" title="0">return strings.TrimSpace(StripFences(txt)), nil</span>
+ <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="7">{
+func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) <span class="cov8" title="7">{
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="7">return strings.TrimSpace(StripFences(txt)), nil</span>
+ <span class="cov8" title="7">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="7">{
+func reqOptsFrom(cfg appconfig.App) []llm.RequestOption <span class="cov8" title="7">{
opts := []llm.RequestOption{llm.WithMaxTokens(cfg.MaxTokens)}
if cfg.CodingTemperature != nil </span><span class="cov5" title="3">{
opts = append(opts, llm.WithTemperature(*cfg.CodingTemperature))
}</span>
- <span class="cov9" title="7">return opts</span>
+ <span class="cov8" title="7">return opts</span>
}
// Timeout helpers to mirror LSP behavior.
-func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov4" title="2">{
+func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov3" title="2">{
return context.WithTimeout(parent, 10*time.Second)
}</span>
@@ -1220,11 +1444,11 @@ import (
)
// Run executes the hexai-action command flow.
-func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov0" title="0">{
+func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov1" title="1">{
logger := log.New(stderr, "hexai-action ", log.LstdFlags|log.Lmsgprefix)
cfg := appconfig.Load(logger)
client, err := llmutils.NewClientFromApp(cfg)
- if err != nil </span><span class="cov0" title="0">{
+ if err != nil </span><span class="cov1" title="1">{
fmt.Fprintf(stderr, logging.AnsiBase+"hexai-action: LLM disabled: %v"+logging.AnsiReset+"\n", err)
return err
}</span>
@@ -1298,9 +1522,9 @@ type item struct {
hotkey rune
}
-func (i item) Title() string <span class="cov0" title="0">{ return i.title }</span>
-func (i item) Description() string <span class="cov0" title="0">{ return i.desc }</span>
-func (i item) FilterValue() string <span class="cov1" title="1">{ return i.title }</span>
+func (i item) Title() string <span class="cov1" title="1">{ return i.title }</span>
+func (i item) Description() string <span class="cov1" title="1">