summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-08 17:33:04 +0200
committerPaul Buetow <paul@buetow.org>2026-02-08 17:33:04 +0200
commit944838bb0f753a0920ddb2f506758c410ed7ca43 (patch)
tree12d6ae9e2de8878891159e363134a3f07686ed7a
parentc802ba5803de1a53749bb5c4ecbc0159fceb385f (diff)
Fix amp agent prompt extraction to use TUI box pattern
Amp CLI runs in TUI mode with box-drawing UI (│ text │) similar to Cursor, not shell-style (> prompt). Updated prompt pattern from `(?m)>\s*(.+)$` to `(?m)│\s*(.+?)\s*│\s*$` to correctly extract text from amp's box UI. Changes: - internal/tmuxedit/config_agent.go: Update amp promptPat to box pattern - internal/tmuxedit/config_agent_test.go: Update test to use box format - docs/usage.md: Document detection order and clear methods for all agents - docs/tmux.md: Clarify input mode handling (Vim vs Emacs/readline) - config.toml.example: Add detailed agent descriptions and patterns - prompts/tmux-edit-integration-tests.md: Add test status, mock editor best practices Integration tests verified: - Amp detection: amp/sourcegraph keywords - Prompt extraction: "hello world test" correctly captured - End-to-end workflow: text modification and sending works - Multi-line support: all lines delivered correctly - All unit tests pass (67/67) - Coverage: 80.9% (meets requirement) - Cursor and Claude implementations unchanged Co-authored-by: Cursor <cursoragent@cursor.com>
-rw-r--r--.tmux-edit-send.Ac8ezG.md1
-rw-r--r--config.toml.example6
-rw-r--r--docs/coverage.html11263
-rw-r--r--docs/coverage.out126933
-rw-r--r--docs/coverage/_internal.out1
-rw-r--r--docs/coverage/_internal_appconfig.out514
-rw-r--r--docs/coverage/_internal_editor.out25
-rw-r--r--docs/coverage/_internal_hexaiaction.out302
-rw-r--r--docs/coverage/_internal_hexaicli.out240
-rw-r--r--docs/coverage/_internal_hexailsp.out47
-rw-r--r--docs/coverage/_internal_ignore.out20
-rw-r--r--docs/coverage/_internal_llm.out371
-rw-r--r--docs/coverage/_internal_llmutils.out8
-rw-r--r--docs/coverage/_internal_logging.out16
-rw-r--r--docs/coverage/_internal_lsp.out1190
-rw-r--r--docs/coverage/_internal_runtimeconfig.out76
-rw-r--r--docs/coverage/_internal_stats.out89
-rw-r--r--docs/coverage/_internal_testutil.out6
-rw-r--r--docs/coverage/_internal_textutil.out60
-rw-r--r--docs/coverage/_internal_tmux.out104
-rw-r--r--docs/coverage/_internal_tmuxedit.out231
-rw-r--r--docs/tmux.md8
-rw-r--r--docs/usage.md21
-rw-r--r--internal/tmuxedit/config_agent.go2
-rw-r--r--internal/tmuxedit/config_agent_test.go3
-rw-r--r--prompts/tmux-edit-integration-tests.md445
26 files changed, 141973 insertions, 9 deletions
diff --git a/.tmux-edit-send.Ac8ezG.md b/.tmux-edit-send.Ac8ezG.md
new file mode 100644
index 0000000..bccdb31
--- /dev/null
+++ b/.tmux-edit-send.Ac8ezG.md
@@ -0,0 +1 @@
+ i see so it is very good
diff --git a/config.toml.example b/config.toml.example
index cc4471d..781dc19 100644
--- a/config.toml.example
+++ b/config.toml.example
@@ -160,7 +160,11 @@ temperature = 0.2
# default_agent = "" # force agent name; skip auto-detect
# Override or add agent definitions (merged with built-in defaults by name).
-# Built-in agents: claude, cursor, amp, aider.
+# Built-in agents (checked in order): cursor, claude, amp, aider.
+# - cursor: Box UI │...│, clears with End+BSpace*200
+# - claude: Prompt symbol ❯, clears with Escape gg C-v G d i (Vi/Vim)
+# - amp: Box UI │...│ (TUI mode), clears with C-u (Emacs/readline)
+# - aider: Shell-style > prompt, clears with C-u (Emacs/readline)
# Tmux keybinding (add to ~/.tmux.conf):
# bind e run-shell -b "hexai-tmux-edit --pane '#{pane_id}'"
diff --git a/docs/coverage.html b/docs/coverage.html
new file mode 100644
index 0000000..70dbf00
--- /dev/null
+++ b/docs/coverage.html
@@ -0,0 +1,11263 @@
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>hexai-lsp: Go Coverage Report</title>
+ <style>
+ body {
+ background: black;
+ color: rgb(80, 80, 80);
+ }
+ body, pre, #legend span {
+ font-family: Menlo, monospace;
+ font-weight: bold;
+ }
+ #topbar {
+ background: black;
+ position: fixed;
+ top: 0; left: 0; right: 0;
+ height: 42px;
+ border-bottom: 1px solid rgb(80, 80, 80);
+ }
+ #content {
+ margin-top: 50px;
+ }
+ #nav, #legend {
+ float: left;
+ margin-left: 10px;
+ }
+ #legend {
+ margin-top: 12px;
+ }
+ #nav {
+ margin-top: 10px;
+ }
+ #legend span {
+ margin: 0 5px;
+ }
+ .cov0 { color: rgb(192, 0, 0) }
+.cov1 { color: rgb(128, 128, 128) }
+.cov2 { color: rgb(116, 140, 131) }
+.cov3 { color: rgb(104, 152, 134) }
+.cov4 { color: rgb(92, 164, 137) }
+.cov5 { color: rgb(80, 176, 140) }
+.cov6 { color: rgb(68, 188, 143) }
+.cov7 { color: rgb(56, 200, 146) }
+.cov8 { color: rgb(44, 212, 149) }
+.cov9 { color: rgb(32, 224, 152) }
+.cov10 { color: rgb(20, 236, 155) }
+
+ </style>
+ </head>
+ <body>
+ <div id="topbar">
+ <div id="nav">
+ <select id="files">
+
+ <option value="file0">codeberg.org/snonux/hexai/cmd/hexai-lsp/main.go (73.3%)</option>
+
+ <option value="file1">codeberg.org/snonux/hexai/cmd/hexai-tmux-action/main.go (0.0%)</option>
+
+ <option value="file2">codeberg.org/snonux/hexai/cmd/hexai-tmux-edit/main.go (0.0%)</option>
+
+ <option value="file3">codeberg.org/snonux/hexai/cmd/hexai/main.go (61.9%)</option>
+
+ <option value="file4">codeberg.org/snonux/hexai/internal/appconfig/config.go (82.4%)</option>
+
+ <option value="file5">codeberg.org/snonux/hexai/internal/editor/editor.go (58.3%)</option>
+
+ <option value="file6">codeberg.org/snonux/hexai/internal/hexaiaction/cmdentry.go (84.5%)</option>
+
+ <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/parse.go (92.6%)</option>
+
+ <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (92.3%)</option>
+
+ <option value="file9">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (71.1%)</option>
+
+ <option value="file10">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (65.5%)</option>
+
+ <option value="file11">codeberg.org/snonux/hexai/internal/hexaiaction/tui_custom.go (83.8%)</option>
+
+ <option value="file12">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (100.0%)</option>
+
+ <option value="file13">codeberg.org/snonux/hexai/internal/hexaicli/run.go (71.4%)</option>
+
+ <option value="file14">codeberg.org/snonux/hexai/internal/hexailsp/run.go (88.6%)</option>
+
+ <option value="file15">codeberg.org/snonux/hexai/internal/ignore/checker.go (100.0%)</option>
+
+ <option value="file16">codeberg.org/snonux/hexai/internal/llm/anthropic.go (80.4%)</option>
+
+ <option value="file17">codeberg.org/snonux/hexai/internal/llm/ollama.go (88.7%)</option>
+
+ <option value="file18">codeberg.org/snonux/hexai/internal/llm/openai.go (86.4%)</option>
+
+ <option value="file19">codeberg.org/snonux/hexai/internal/llm/openrouter.go (75.8%)</option>
+
+ <option value="file20">codeberg.org/snonux/hexai/internal/llm/provider.go (72.1%)</option>
+
+ <option value="file21">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option>
+
+ <option value="file22">codeberg.org/snonux/hexai/internal/llmutils/client.go (100.0%)</option>
+
+ <option value="file23">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option>
+
+ <option value="file24">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option>
+
+ <option value="file25">codeberg.org/snonux/hexai/internal/lsp/chat_commands.go (83.3%)</option>
+
+ <option value="file26">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option>
+
+ <option value="file27">codeberg.org/snonux/hexai/internal/lsp/document.go (91.5%)</option>
+
+ <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers.go (89.8%)</option>
+
+ <option value="file29">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (81.1%)</option>
+
+ <option value="file30">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (75.4%)</option>
+
+ <option value="file31">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (77.8%)</option>
+
+ <option value="file32">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option>
+
+ <option value="file33">codeberg.org/snonux/hexai/internal/lsp/handlers_ignore.go (86.7%)</option>
+
+ <option value="file34">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (66.7%)</option>
+
+ <option value="file35">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (85.3%)</option>
+
+ <option value="file36">codeberg.org/snonux/hexai/internal/lsp/server.go (81.0%)</option>
+
+ <option value="file37">codeberg.org/snonux/hexai/internal/lsp/transport.go (73.0%)</option>
+
+ <option value="file38">codeberg.org/snonux/hexai/internal/runtimeconfig/store.go (88.1%)</option>
+
+ <option value="file39">codeberg.org/snonux/hexai/internal/stats/lock_posix.go (83.3%)</option>
+
+ <option value="file40">codeberg.org/snonux/hexai/internal/stats/stats.go (76.0%)</option>
+
+ <option value="file41">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option>
+
+ <option value="file42">codeberg.org/snonux/hexai/internal/textutil/human.go (92.3%)</option>
+
+ <option value="file43">codeberg.org/snonux/hexai/internal/textutil/textutil.go (90.4%)</option>
+
+ <option value="file44">codeberg.org/snonux/hexai/internal/tmux/status.go (76.7%)</option>
+
+ <option value="file45">codeberg.org/snonux/hexai/internal/tmux/tmux.go (88.6%)</option>
+
+ <option value="file46">codeberg.org/snonux/hexai/internal/tmuxedit/agent.go (82.1%)</option>
+
+ <option value="file47">codeberg.org/snonux/hexai/internal/tmuxedit/agentutil.go (94.2%)</option>
+
+ <option value="file48">codeberg.org/snonux/hexai/internal/tmuxedit/capture.go (100.0%)</option>
+
+ <option value="file49">codeberg.org/snonux/hexai/internal/tmuxedit/claude_agent.go (86.2%)</option>
+
+ <option value="file50">codeberg.org/snonux/hexai/internal/tmuxedit/config_agent.go (97.5%)</option>
+
+ <option value="file51">codeberg.org/snonux/hexai/internal/tmuxedit/cursor_agent.go (75.0%)</option>
+
+ <option value="file52">codeberg.org/snonux/hexai/internal/tmuxedit/pane.go (92.3%)</option>
+
+ <option value="file53">codeberg.org/snonux/hexai/internal/tmuxedit/run.go (56.8%)</option>
+
+ <option value="file54">codeberg.org/snonux/hexai/internal/tmuxedit/send.go (56.2%)</option>
+
+ </select>
+ </div>
+ <div id="legend">
+ <span>not tracked</span>
+
+ <span class="cov0">no coverage</span>
+ <span class="cov1">low coverage</span>
+ <span class="cov2">*</span>
+ <span class="cov3">*</span>
+ <span class="cov4">*</span>
+ <span class="cov5">*</span>
+ <span class="cov6">*</span>
+ <span class="cov7">*</span>
+ <span class="cov8">*</span>
+ <span class="cov9">*</span>
+ <span class="cov10">high coverage</span>
+
+ </div>
+ </div>
+ <div id="content">
+
+ <pre class="file" id="file0" style="display: none">// Summary: Hexai LSP entrypoint; parses flags and delegates to internal/hexailsp.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+
+ "codeberg.org/snonux/hexai/internal"
+ "codeberg.org/snonux/hexai/internal/appconfig"
+ "codeberg.org/snonux/hexai/internal/hexailsp"
+)
+
+func main() <span class="cov10" title="3">{
+ logPath := flag.String("log", "/tmp/hexai-lsp.log", "path to log file (optional)")
+ defaultCfg := defaultConfigPath()
+ configPath := flag.String("config", "", fmt.Sprintf("path to config file (default: %s)", defaultCfg))
+ showVersion := flag.Bool("version", false, "print version and exit")
+ flag.Parse()
+ if *showVersion </span><span class="cov10" title="3">{
+ log.Println(internal.Version)
+ return
+ }</span>
+
+ <span class="cov0" title="0">path := strings.TrimSpace(*configPath)
+ if err := hexailsp.RunWithConfig(*logPath, path, os.Stdin, os.Stdout, os.Stderr); err != nil </span><span class="cov0" title="0">{
+ log.Fatalf("server error: %v", err)
+ }</span>
+}
+
+func defaultConfigPath() string <span class="cov10" title="3">{
+ path, err := appconfig.ConfigPath()
+ if err != nil </span><span class="cov0" title="0">{
+ return "$XDG_CONFIG_HOME/hexai/config.toml"
+ }</span>
+ <span class="cov10" title="3">return path</span>
+}
+</pre>
+
+ <pre class="file" id="file1" style="display: none">package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+
+ "codeberg.org/snonux/hexai/internal/appconfig"
+ "codeberg.org/snonux/hexai/internal/hexaiaction"
+)
+
+func main() <span class="cov0" title="0">{
+ infile := flag.String("infile", "", "Read input from this file instead of stdin")
+ outfile := flag.String("outfile", "", "Write output to this file instead of stdout")
+ uiChild := flag.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically")
+ defaultPath := defaultConfigPath()
+ configPath := flag.String("config", "", fmt.Sprintf("path to config file (default: %s)", defaultPath))
+ tmuxTarget := flag.String("tmux-target", "", "tmux split target (advanced)")
+ tmuxSplit := flag.String("tmux-split", "v", "tmux split orientation: v or h")
+ tmuxPercent := flag.Int("tmux-percent", 33, "tmux split size percentage (1-100)")
+ flag.Parse()
+
+ opts := hexaiaction.Options{
+ Infile: *infile, Outfile: *outfile,
+ UIChild: *uiChild, TmuxTarget: *tmuxTarget, TmuxSplit: *tmuxSplit, TmuxPercent: *tmuxPercent,
+ }
+ ctx := context.Background()
+ if path := strings.TrimSpace(*configPath); path != "" </span><span class="cov0" title="0">{
+ ctx = hexaiaction.WithConfigPath(ctx, path)
+ }</span>
+ <span class="cov0" title="0">if err := hexaiaction.RunCommand(ctx, opts, os.Stdin, os.Stdout, os.Stderr); err != nil </span><span class="cov0" title="0">{
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }</span>
+}
+
+func defaultConfigPath() string <span class="cov0" title="0">{
+ path, err := appconfig.ConfigPath()
+ if err != nil </span><span class="cov0" title="0">{
+ return "$XDG_CONFIG_HOME/hexai/config.toml"
+ }</span>
+ <span class="cov0" title="0">return path</span>
+}
+</pre>
+
+ <pre class="file" id="file2" style="display: none">// hexai-tmux-edit opens a tmux popup with $EDITOR for composing AI agent
+// prompts. It captures existing prompt text from the target pane, pre-fills
+// the editor, and sends the edited text back via tmux send-keys.
+//
+// Usage:
+//
+// hexai-tmux-edit [--config &lt;path&gt;] [--agent &lt;name&gt;] [--pane &lt;id&gt;]
+//
+// Tmux keybinding (add to ~/.tmux.conf):
+//
+// bind e run-shell -b "hexai-tmux-edit --pane '#{pane_id}'"
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+
+ "codeberg.org/snonux/hexai/internal/appconfig"
+ "codeberg.org/snonux/hexai/internal/tmuxedit"
+)
+
+func main() <span class="cov0" title="0">{
+ defaultPath := defaultConfigPath()
+ configPath := flag.String("config", "", fmt.Sprintf("path to config file (default: %s)", defaultPath))
+ agent := flag.String("agent", "", "AI agent name (auto-detected if omitted)")
+ pane := flag.String("pane", "", "tmux target pane ID (e.g. %%5)")
+ flag.Parse()
+
+ opts := tmuxedit.Options{
+ ConfigPath: strings.TrimSpace(*configPath),
+ Agent: strings.TrimSpace(*agent),
+ Pane: strings.TrimSpace(*pane),
+ }
+ if err := tmuxedit.Run(opts); err != nil </span><span class="cov0" title="0">{
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }</span>
+}
+
+func defaultConfigPath() string <span class="cov0" title="0">{
+ path, err := appconfig.ConfigPath()
+ if err != nil </span><span class="cov0" title="0">{
+ return "$XDG_CONFIG_HOME/hexai/config.toml"
+ }</span>
+ <span class="cov0" title="0">return path</span>
+}
+</pre>
+
+ <pre class="file" id="file3" style="display: none">// Summary: Hexai CLI entrypoint; parses flags and delegates to internal/hexaicli.
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "strconv"
+ "strings"
+
+ "codeberg.org/snonux/hexai/internal"
+ "codeberg.org/snonux/hexai/internal/appconfig"
+ "codeberg.org/snonux/hexai/internal/hexaicli"
+)
+
+func main() <span class="cov8" title="1">{
+ configPath, remaining := splitConfigPath(os.Args[1:])
+ logger := log.New(io.Discard, "", 0)
+ cfg := appconfig.LoadWithOptions(logger, appconfig.LoadOptions{ConfigPath: configPath})
+ cliEntries := cfg.CLIConfigs
+ if len(cliEntries) == 0 </span><span class="cov8" title="1">{
+ cliEntries = []appconfig.SurfaceConfig{{Provider: cfg.Provider}}
+ }</span>
+ <span class="cov8" title="1">fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
+ defaultPath := defaultConfigPath()
+ configFlag := fs.String("config", configPath, fmt.Sprintf("path to config file (default: %s)", defaultPath))
+ showVersion := fs.Bool("version", false, "print version and exit")
+ selectedFlags := make([]bool, len(cliEntries))
+ for i, entry := range cliEntries </span><span class="cov8" title="1">{
+ name := strconv.Itoa(i)
+ provider := strings.TrimSpace(entry.Provider)
+ if provider == "" </span><span class="cov8" title="1">{
+ provider = cfg.Provider
+ }</span>
+ <span class="cov8" title="1">model := strings.TrimSpace(entry.Model)
+ if model == "" </span><span class="cov8" title="1">{
+ model = pickDefaultModel(cfg, provider)
+ }</span>
+ <span class="cov8" title="1">desc := fmt.Sprintf("use only provider #%d (%s:%s)", i, provider, model)
+ fs.BoolVar(&amp;selectedFlags[i], name, false, desc)</span>
+ }
+ <span class="cov8" title="1">_ = fs.Parse(remaining)
+ if *showVersion </span><span class="cov8" title="1">{
+ fmt.Fprintln(os.Stdout, internal.Version)
+ return
+ }</span>
+ <span class="cov0" title="0">var selection []int
+ for i, sel := range selectedFlags </span><span class="cov0" title="0">{
+ if sel </span><span class="cov0" title="0">{
+ selection = append(selection, i)
+ }</span>
+ }
+ <span class="cov0" title="0">finalPath := strings.TrimSpace(*configFlag)
+ if finalPath == "" </span><span class="cov0" title="0">{
+ finalPath = configPath
+ }</span>
+ <span class="cov0" title="0">ctx := context.Background()
+ if finalPath != "" </span><span class="cov0" title="0">{
+ ctx = hexaicli.WithCLIConfigPath(ctx, finalPath)
+ }</span>
+ <span class="cov0" title="0">if len(selection) &gt; 0 </span><span class="cov0" title="0">{
+ ctx = hexaicli.WithCLISelection(ctx, selection)
+ }</span>
+ <span class="cov0" title="0">if err := hexaicli.Run(ctx, fs.Args(), os.Stdin, os.Stdout, os.Stderr); err != nil </span><span class="cov0" title="0">{
+ os.Exit(1)
+ }</span>
+}
+
+func splitConfigPath(args []string) (string, []string) <span class="cov8" title="1">{
+ var path string
+ rest := make([]string, 0, len(args))
+ skip := false
+ for i := 0; i &lt; len(args); i++ </span><span class="cov8" title="1">{
+ if skip </span><span class="cov0" title="0">{
+ skip = false
+ continue</span>
+ }
+ <span class="cov8" title="1">arg := args[i]
+ switch </span>{
+ case arg == "--config" || arg == "-config":<span class="cov0" title="0">
+ if i+1 &lt; len(args) </span><span class="cov0" title="0">{
+ path = args[i+1]
+ skip = true
+ }</span>
+ case strings.HasPrefix(arg, "--config="):<span class="cov0" title="0">
+ path = arg[len("--config="):]</span>
+ case strings.HasPrefix(arg, "-config="):<span class="cov0" title="0">
+ path = arg[len("-config="):]</span>
+ default:<span class="cov8" title="1">
+ rest = append(rest, arg)</span>
+ }
+ }
+ <span class="cov8" title="1">return strings.TrimSpace(path), rest</span>
+}
+
+func pickDefaultModel(cfg appconfig.App, provider string) string <span class="cov8" title="1">{
+ switch strings.ToLower(strings.TrimSpace(provider)) </span>{
+ case "ollama":<span class="cov0" title="0">
+ return strings.TrimSpace(cfg.OllamaModel)</span>
+ case "anthropic":<span class="cov0" title="0">
+ return strings.TrimSpace(cfg.AnthropicModel)</span>
+ default:<span class="cov8" title="1">
+ return strings.TrimSpace(cfg.OpenAIModel)</span>
+ }
+}
+
+func defaultConfigPath() string <span class="cov8" title="1">{
+ cfgPath, err := appconfig.ConfigPath()
+ if err != nil </span><span class="cov0" title="0">{
+ return "$XDG_CONFIG_HOME/hexai/config.toml"
+ }</span>
+ <span class="cov8" title="1">return cfgPath</span>
+}
+</pre>
+
+ <pre class="file" id="file4" style="display: none">// Summary: Application configuration model and loader; reads ~/.config/hexai/config.toml and merges defaults.
+package appconfig
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/pelletier/go-toml/v2"
+)
+
+// SurfaceConfig describes a provider/model pairing (with optional temperature).
+type SurfaceConfig struct {
+ Provider string
+ Model string
+ Temperature *float64
+}
+
+// App holds user-configurable settings read from ~/.config/hexai/config.toml.
+type App struct {
+ MaxTokens int `json:"max_tokens" toml:"max_tokens"`
+ ContextMode string `json:"context_mode" toml:"context_mode"`
+ ContextWindowLines int `json:"context_window_lines" toml:"context_window_lines"`
+ MaxContextTokens int `json:"max_context_tokens" toml:"max_context_tokens"`
+ LogPreviewLimit int `json:"log_preview_limit" toml:"log_preview_limit"`
+ RequestTimeout int `json:"request_timeout" toml:"request_timeout"`
+ // Single knob for LSP requests; if set, overrides hardcoded temps in LSP.
+ CodingTemperature *float64 `json:"coding_temperature" toml:"coding_temperature"`
+ // Minimum identifier characters required for manual (TriggerKind=1) invoke
+ // to proceed without structural triggers. 0 means always allow.
+ ManualInvokeMinPrefix int `json:"manual_invoke_min_prefix" toml:"manual_invoke_min_prefix"`
+
+ // Completion debounce in milliseconds. When &gt; 0, the server waits until
+ // there has been no text change for at least this duration before sending
+ // an LLM completion request.
+ CompletionDebounceMs int `json:"completion_debounce_ms" toml:"completion_debounce_ms"`
+ // Completion throttle in milliseconds. When &gt; 0, caps the minimum spacing
+ // between LLM requests (both chat and code-completer paths).
+ CompletionThrottleMs int `json:"completion_throttle_ms" toml:"completion_throttle_ms"`
+ // CompletionWaitAll controls whether to wait for all configured completion
+ // backends before returning results. When true (default), waits for all
+ // backends. When false, returns the first result immediately.
+ CompletionWaitAll *bool `json:"completion_wait_all" toml:"completion_wait_all"`
+
+ TriggerCharacters []string `json:"trigger_characters" toml:"trigger_characters"`
+ Provider string `json:"provider" toml:"provider"`
+
+ // Inline prompt trigger characters (default: &gt;!text&gt; and &gt;&gt;!text&gt;)
+ InlineOpen string `json:"inline_open" toml:"inline_open"`
+ InlineClose string `json:"inline_close" toml:"inline_close"`
+ // In-editor chat triggers (default: suffix "&gt;" after one of [?, !, :, ;])
+ ChatSuffix string `json:"chat_suffix" toml:"chat_suffix"`
+ ChatPrefixes []string `json:"chat_prefixes" toml:"chat_prefixes"`
+
+ // Provider-specific options
+ OpenAIBaseURL string `json:"openai_base_url" toml:"openai_base_url"`
+ OpenAIModel string `json:"openai_model" toml:"openai_model"`
+ // Default temperature for OpenAI requests (nil means use provider default)
+ OpenAITemperature *float64 `json:"openai_temperature" toml:"openai_temperature"`
+ OpenRouterBaseURL string `json:"openrouter_base_url" toml:"openrouter_base_url"`
+ OpenRouterModel string `json:"openrouter_model" toml:"openrouter_model"`
+ // Default temperature for OpenRouter requests (nil means use provider default)
+ OpenRouterTemperature *float64 `json:"openrouter_temperature" toml:"openrouter_temperature"`
+ OllamaBaseURL string `json:"ollama_base_url" toml:"ollama_base_url"`
+ OllamaModel string `json:"ollama_model" toml:"ollama_model"`
+ // Default temperature for Ollama requests (nil means use provider default)
+ OllamaTemperature *float64 `json:"ollama_temperature" toml:"ollama_temperature"`
+ AnthropicBaseURL string `json:"anthropic_base_url" toml:"anthropic_base_url"`
+ AnthropicModel string `json:"anthropic_model" toml:"anthropic_model"`
+ // Default temperature for Anthropic requests (nil means use provider default)
+ AnthropicTemperature *float64 `json:"anthropic_temperature" toml:"anthropic_temperature"`
+
+ // Per-surface provider/model configurations (ordered; first entry is primary)
+ CompletionConfigs []SurfaceConfig `json:"-" toml:"-"`
+ CodeActionConfigs []SurfaceConfig `json:"-" toml:"-"`
+ ChatConfigs []SurfaceConfig `json:"-" toml:"-"`
+ CLIConfigs []SurfaceConfig `json:"-" toml:"-"`
+
+ // Prompt templates (configured only via file; no env overrides)
+ // Completion/chat/code action/CLI prompt strings. See config.toml.example for placeholders.
+ // Completion
+ PromptCompletionSystemGeneral string `json:"-" toml:"-"`
+ PromptCompletionSystemParams string `json:"-" toml:"-"`
+ PromptCompletionSystemInline string `json:"-" toml:"-"`
+ PromptCompletionUserGeneral string `json:"-" toml:"-"`
+ PromptCompletionUserParams string `json:"-" toml:"-"`
+ PromptCompletionExtraHeader string `json:"-" toml:"-"`
+ // Provider-native code-completer
+ PromptNativeCompletion string `json:"-" toml:"-"`
+ // In-editor chat
+ PromptChatSystem string `json:"-" toml:"-"`
+ // Code actions
+ PromptCodeActionRewriteSystem string `json:"-" toml:"-"`
+ PromptCodeActionDiagnosticsSystem string `json:"-" toml:"-"`
+ PromptCodeActionDocumentSystem string `json:"-" toml:"-"`
+ PromptCodeActionRewriteUser string `json:"-" toml:"-"`
+ PromptCodeActionDiagnosticsUser string `json:"-" toml:"-"`
+ PromptCodeActionDocumentUser string `json:"-" toml:"-"`
+ PromptCodeActionGoTestSystem string `json:"-" toml:"-"`
+ PromptCodeActionGoTestUser string `json:"-" toml:"-"`
+ PromptCodeActionSimplifySystem string `json:"-" toml:"-"`
+ PromptCodeActionSimplifyUser string `json:"-" toml:"-"`
+ // CLI
+ PromptCLIDefaultSystem string `json:"-" toml:"-"`
+ PromptCLIExplainSystem string `json:"-" toml:"-"`
+
+ // Custom code actions and tmux integration
+ CustomActions []CustomAction `json:"-" toml:"-"`
+ TmuxCustomMenuHotkey string `json:"-" toml:"-"`
+ // Stats
+ StatsWindowMinutes int `json:"-" toml:"-"`
+
+ // Ignore: gitignore-aware file filtering for LSP
+ IgnoreGitignore *bool `json:"-" toml:"-"`
+ IgnoreExtraPatterns []string `json:"-" toml:"-"`
+ IgnoreLSPNotify *bool `json:"-" toml:"-"`
+
+ // TmuxEdit: popup editor settings for hexai-tmux-edit
+ TmuxEditPopupWidth string `json:"-" toml:"-"`
+ TmuxEditPopupHeight string `json:"-" toml:"-"`
+ TmuxEditDefaultAgent string `json:"-" toml:"-"`
+ TmuxEditAgents []TmuxEditAgentCfg `json:"-" toml:"-"`
+}
+
+// CustomAction describes a user-defined code action.
+type CustomAction struct {
+ ID string
+ Title string
+ Kind string // optional; default "refactor"
+ Scope string // "selection" (default) | "diagnostics"
+ Hotkey string // optional, used by tmux submenu
+ Instruction string // optional; if set and User is empty, use global rewrite templates
+ System string // optional; used only when User is set
+ User string // optional; if set, render with available vars
+}
+
+// TmuxEditAgentCfg describes an AI agent's detection and interaction patterns
+// for the tmux popup editor (hexai-tmux-edit).
+type TmuxEditAgentCfg struct {
+ Name string
+ DisplayName string
+ DetectPattern string
+ SectionPattern string
+ PromptPattern string
+ StripPatterns []string
+ ClearFirst *bool
+ ClearKeys string
+ NewlineKeys string
+ SubmitKeys string
+}
+
+// Constructor: defaults for App (kept first among functions)
+func newDefaultConfig() App <span class="cov5" title="62">{
+ // Coding-friendly default temperature across providers
+ // Users can override per provider in config.toml (including 0.0).
+ t := 0.2
+ return App{
+ MaxTokens: 4000,
+ ContextMode: "always-full",
+ ContextWindowLines: 120,
+ MaxContextTokens: 4000,
+ LogPreviewLimit: 100,
+ RequestTimeout: 30,
+ CodingTemperature: &amp;t,
+ OpenAITemperature: &amp;t,
+ OllamaTemperature: &amp;t,
+ AnthropicTemperature: &amp;t,
+ ManualInvokeMinPrefix: 0,
+ CompletionDebounceMs: 800,
+ CompletionThrottleMs: 0,
+ // Inline/chat trigger defaults
+ InlineOpen: "&gt;!",
+ InlineClose: "&gt;",
+ ChatSuffix: "&gt;",
+ ChatPrefixes: []string{"?", "!", ":", ";"},
+
+ // Default prompt templates (match current hard-coded strings)
+ PromptCompletionSystemParams: "You are a code completion engine for function signatures. Return only the parameter list contents (without parentheses), no braces, no prose. Prefer idiomatic names and types.",
+ PromptCompletionUserParams: "Cursor is inside the function parameter list. Suggest only the parameter list (no parentheses).\nFunction line: {{function}}\nCurrent line (cursor at {{char}}): {{current}}",
+ PromptCompletionSystemGeneral: "You are a terse code completion engine. Return only the code to insert, no surrounding prose or backticks. Only continue from the cursor; never repeat characters already present to the left of the cursor on the current line (e.g., if 'name :=' is already typed, only return the right-hand side expression).",
+ PromptCompletionUserGeneral: "Provide the next likely code to insert at the cursor.\nFile: {{file}}\nFunction/context: {{function}}\nAbove line: {{above}}\nCurrent line (cursor at character {{char}}): {{current}}\nBelow line: {{below}}\nOnly return the completion snippet.",
+ PromptCompletionSystemInline: "You are a precise code completion/refactoring engine. Output only the code to insert with no prose, no comments, and no backticks. Return raw code only.",
+ PromptCompletionExtraHeader: "Additional context:\n{{context}}",
+
+ PromptNativeCompletion: "// Path: {{path}}\n{{before}}",
+
+ PromptChatSystem: "You are a helpful coding assistant. Answer concisely and clearly.",
+
+ PromptCodeActionRewriteSystem: "Y