summaryrefslogtreecommitdiff
path: root/docs/coverage.html
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/coverage.html
parent7c0266e94378f6121719939c6d53915eb72eed3e (diff)
tiding up
Diffstat (limited to 'docs/coverage.html')
-rw-r--r--docs/coverage.html513
1 files changed, 412 insertions, 101 deletions
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">{ return i.desc }</span>
+func (i item) FilterValue() string <span class="cov8" title="3">{ return i.title }</span>
type model struct {
list list.Model
@@ -1308,7 +1532,7 @@ type model struct {
done bool
}
-func newModel() model <span class="cov10" title="3">{
+func newModel() model <span class="cov10" title="4">{
items := []list.Item{
item{title: "Rewrite selection", desc: "", kind: ActionRewrite, hotkey: 'r'},
item{title: "Document code", desc: "", kind: ActionDocument, hotkey: 'c'},
@@ -1323,21 +1547,21 @@ func newModel() model <span class="cov10" title="3">{
return model{list: l}
}</span>
-func (m model) Init() tea.Cmd <span class="cov0" title="0">{ return nil }</span>
+func (m model) Init() tea.Cmd <span class="cov1" title="1">{ return nil }</span>
-func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) <span class="cov0" title="0">{
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) <span class="cov1" title="1">{
switch msg := msg.(type) </span>{
case tea.KeyMsg:<span class="cov0" title="0">
return handleKey(m, msg)</span>
- case tea.WindowSizeMsg:<span class="cov0" title="0">
+ case tea.WindowSizeMsg:<span class="cov1" title="1">
m.list.SetSize(msg.Width, msg.Height)</span>
}
- <span class="cov0" title="0">var cmd tea.Cmd
+ <span class="cov1" title="1">var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd</span>
}
-func handleKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) <span class="cov10" title="3">{
+func handleKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) <span class="cov8" title="3">{
raw := msg.String()
low := strings.ToLower(raw)
switch low </span>{
@@ -1377,11 +1601,11 @@ func handleKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) <span class="cov10"
<span class="cov1" title="1">return m, nil</span>
}
-func (m model) View() string <span class="cov0" title="0">{
+func (m model) View() string <span class="cov1" title="1">{
if m.done </span><span class="cov0" title="0">{
return ""
}</span>
- <span class="cov0" title="0">return m.list.View()</span>
+ <span class="cov1" title="1">return m.list.View()</span>
}
// RunTUI returns the chosen ActionKind.
@@ -1420,21 +1644,21 @@ var (
cursorStyle = lipgloss.NewStyle().Bold(true)
)
-func (oneLineDelegate) Height() int <span class="cov8" title="10">{ return 1 }</span>
-func (oneLineDelegate) Spacing() int <span class="cov10" title="16">{ return 0 }</span>
-func (oneLineDelegate) Update(tea.Msg, *list.Model