summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/appconfig/config.go70
-rw-r--r--internal/appconfig/config_test.go72
-rw-r--r--internal/hexaicli/run_test.go2
-rw-r--r--internal/hexaicli/testhelpers_test.go11
4 files changed, 76 insertions, 79 deletions
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go
index 92fdf19..9404607 100644
--- a/internal/appconfig/config.go
+++ b/internal/appconfig/config.go
@@ -1,8 +1,7 @@
-// Summary: Application configuration model and loader; reads ~/.config/hexai/config.json and merges defaults.
+// Summary: Application configuration model and loader; reads ~/.config/hexai/config.toml and merges defaults.
package appconfig
import (
- "encoding/json"
"fmt"
"log"
"os"
@@ -10,58 +9,60 @@ import (
"slices"
"strconv"
"strings"
+
+ "github.com/pelletier/go-toml/v2"
)
-// App holds user-configurable settings read from ~/.config/hexai/config.json.
+// App holds user-configurable settings read from ~/.config/hexai/config.toml.
type App struct {
- MaxTokens int `json:"max_tokens"`
- ContextMode string `json:"context_mode"`
- ContextWindowLines int `json:"context_window_lines"`
- MaxContextTokens int `json:"max_context_tokens"`
- LogPreviewLimit int `json:"log_preview_limit"`
+ 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"`
// Single knob for LSP requests; if set, overrides hardcoded temps in LSP.
- CodingTemperature *float64 `json:"coding_temperature"`
+ 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"`
+ ManualInvokeMinPrefix int `json:"manual_invoke_min_prefix" toml:"manual_invoke_min_prefix"`
// Completion debounce in milliseconds. When > 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"`
+ CompletionDebounceMs int `json:"completion_debounce_ms" toml:"completion_debounce_ms"`
// Completion throttle in milliseconds. When > 0, caps the minimum spacing
// between LLM requests (both chat and code-completer paths).
- CompletionThrottleMs int `json:"completion_throttle_ms"`
+ CompletionThrottleMs int `json:"completion_throttle_ms" toml:"completion_throttle_ms"`
- TriggerCharacters []string `json:"trigger_characters"`
- Provider string `json:"provider"`
+ TriggerCharacters []string `json:"trigger_characters" toml:"trigger_characters"`
+ Provider string `json:"provider" toml:"provider"`
// Inline prompt trigger characters (default: >text> and >>text>)
- InlineOpen string `json:"inline_open"`
- InlineClose string `json:"inline_close"`
+ InlineOpen string `json:"inline_open" toml:"inline_open"`
+ InlineClose string `json:"inline_close" toml:"inline_close"`
// In-editor chat triggers (default: suffix ">" after one of [?, !, :, ;])
- ChatSuffix string `json:"chat_suffix"`
- ChatPrefixes []string `json:"chat_prefixes"`
+ 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"`
- OpenAIModel string `json:"openai_model"`
+ 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"`
- OllamaBaseURL string `json:"ollama_base_url"`
- OllamaModel string `json:"ollama_model"`
+ OpenAITemperature *float64 `json:"openai_temperature" toml:"openai_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"`
- CopilotBaseURL string `json:"copilot_base_url"`
- CopilotModel string `json:"copilot_model"`
+ OllamaTemperature *float64 `json:"ollama_temperature" toml:"ollama_temperature"`
+ CopilotBaseURL string `json:"copilot_base_url" toml:"copilot_base_url"`
+ CopilotModel string `json:"copilot_model" toml:"copilot_model"`
// Default temperature for Copilot requests (nil means use provider default)
- CopilotTemperature *float64 `json:"copilot_temperature"`
+ CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_temperature"`
}
// Constructor: defaults for App (kept first among functions)
func newDefaultConfig() App {
// Coding-friendly default temperature across providers
- // Users can override per provider in config.json (including 0.0).
+ // Users can override per provider in config.toml (including 0.0).
t := 0.2
return App{
MaxTokens: 4000,
@@ -116,20 +117,23 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) {
f, err := os.Open(path)
if err != nil {
if !os.IsNotExist(err) && logger != nil {
- logger.Printf("cannot open config file %s: %v", path, err)
+ logger.Printf("cannot open TOML config file %s: %v", path, err)
}
return nil, err
}
defer f.Close()
- dec := json.NewDecoder(f)
+ dec := toml.NewDecoder(f)
var fileCfg App
if err := dec.Decode(&fileCfg); err != nil {
if logger != nil {
- logger.Printf("invalid config file %s: %v", path, err)
+ logger.Printf("invalid TOML config file %s: %v", path, err)
}
return nil, err
}
+ if logger != nil {
+ logger.Printf("loaded configuration from %s (TOML)", path)
+ }
return &fileCfg, nil
}
@@ -221,13 +225,13 @@ func (a *App) mergeProviderFields(other *App) {
func getConfigPath() (string, error) {
var configPath string
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
- configPath = filepath.Join(xdgConfigHome, "hexai", "config.json")
+ configPath = filepath.Join(xdgConfigHome, "hexai", "config.toml")
} else {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("cannot find user home directory: %v", err)
}
- configPath = filepath.Join(home, ".config", "hexai", "config.json")
+ configPath = filepath.Join(home, ".config", "hexai", "config.toml")
}
return configPath, nil
}
diff --git a/internal/appconfig/config_test.go b/internal/appconfig/config_test.go
index f2e3f7a..bdf86da 100644
--- a/internal/appconfig/config_test.go
+++ b/internal/appconfig/config_test.go
@@ -1,7 +1,6 @@
package appconfig
import (
- "encoding/json"
"io"
"log"
"os"
@@ -13,19 +12,13 @@ import (
func newLogger() *log.Logger { return log.New(io.Discard, "", 0) }
-func writeJSON(t *testing.T, path string, v any) {
+func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
- f, err := os.Create(path)
- if err != nil {
- t.Fatalf("create: %v", err)
- }
- defer f.Close()
- enc := json.NewEncoder(f)
- if err := enc.Encode(v); err != nil {
- t.Fatalf("encode json: %v", err)
+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
+ t.Fatalf("write: %v", err)
}
}
@@ -59,31 +52,30 @@ func TestLoad_Defaults_WithLogger_NoFile_NoEnv(t *testing.T) {
func TestLoad_FileMerge_And_EnvOverride(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)
- cfgPath := filepath.Join(dir, "hexai", "config.json")
- temp0 := 0.0
- fileCfg := App{
- MaxTokens: 123,
- ContextMode: "file-on-new-func",
- ContextWindowLines: 50,
- MaxContextTokens: 999,
- LogPreviewLimit: 0,
- CodingTemperature: &temp0,
- ManualInvokeMinPrefix: 2,
- CompletionDebounceMs: 150,
- CompletionThrottleMs: 300,
- TriggerCharacters: []string{".", ":"},
- Provider: "openai",
- OpenAIBaseURL: "https://api.example",
- OpenAIModel: "gpt-x",
- OpenAITemperature: &temp0,
- OllamaBaseURL: "http://ollama",
- OllamaModel: "llama",
- OllamaTemperature: &temp0,
- CopilotBaseURL: "http://copilot",
- CopilotModel: "ghost",
- CopilotTemperature: &temp0,
- }
- writeJSON(t, cfgPath, fileCfg)
+ cfgPath := filepath.Join(dir, "hexai", "config.toml")
+ // file configuration in TOML
+ writeFile(t, cfgPath, `
+max_tokens = 123
+context_mode = "file-on-new-func"
+context_window_lines = 50
+max_context_tokens = 999
+log_preview_limit = 0
+coding_temperature = 0.0
+manual_invoke_min_prefix = 2
+completion_debounce_ms = 150
+completion_throttle_ms = 300
+trigger_characters = [".", ":"]
+provider = "openai"
+openai_base_url = "https://api.example"
+openai_model = "gpt-x"
+openai_temperature = 0.0
+ollama_base_url = "http://ollama"
+ollama_model = "llama"
+ollama_temperature = 0.0
+copilot_base_url = "http://copilot"
+copilot_model = "ghost"
+copilot_temperature = 0.0
+`)
// Env overrides take precedence
withEnv(t, "HEXAI_MAX_TOKENS", "321")
@@ -163,23 +155,23 @@ func TestGetConfigPath_XDG(t *testing.T) {
if err != nil {
t.Fatalf("getConfigPath: %v", err)
}
- if !strings.HasPrefix(path, filepath.Join(dir, "hexai")) || !strings.HasSuffix(path, "config.json") {
+ if !strings.HasPrefix(path, filepath.Join(dir, "hexai")) || !strings.HasSuffix(path, "config.toml") {
t.Fatalf("unexpected path: %s", path)
}
}
-func TestLoadFromFile_InvalidJSON(t *testing.T) {
+func TestLoadFromFile_InvalidTOML(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)
- cfgPath := filepath.Join(dir, "hexai", "config.json")
+ cfgPath := filepath.Join(dir, "hexai", "config.toml")
if err := os.MkdirAll(filepath.Dir(cfgPath), 0o755); err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(cfgPath, []byte("{ invalid"), 0o644); err != nil {
+ if err := os.WriteFile(cfgPath, []byte("invalid ="), 0o644); err != nil {
t.Fatal(err)
}
_, err := loadFromFile(cfgPath, newLogger())
if err == nil {
- t.Fatalf("expected error for invalid JSON")
+ t.Fatalf("expected error for invalid TOML")
}
}
diff --git a/internal/hexaicli/run_test.go b/internal/hexaicli/run_test.go
index 77daa8b..d192850 100644
--- a/internal/hexaicli/run_test.go
+++ b/internal/hexaicli/run_test.go
@@ -107,7 +107,7 @@ func TestRunWithClient_ErrorPrint(t *testing.T) {
func TestRun_OpenAI_NoKey_ShowsError(t *testing.T) {
dir := testingTempDir(t)
// write config with provider=openai
- writeJSON(t, filepath.Join(dir, "hexai", "config.json"), map[string]any{"provider": "openai", "openai_model": "gpt-x"})
+ writeTOML(t, filepath.Join(dir, "hexai", "config.toml"), map[string]string{"provider": "openai", "openai_model": "gpt-x"})
t.Setenv("XDG_CONFIG_HOME", dir)
// Ensure no OpenAI API key is present in environment
t.Setenv("HEXAI_OPENAI_API_KEY", "")
diff --git a/internal/hexaicli/testhelpers_test.go b/internal/hexaicli/testhelpers_test.go
index 512a3ba..93f1e3d 100644
--- a/internal/hexaicli/testhelpers_test.go
+++ b/internal/hexaicli/testhelpers_test.go
@@ -3,7 +3,6 @@ package hexaicli
import (
"context"
- "encoding/json"
"os"
"path/filepath"
"testing"
@@ -62,8 +61,8 @@ func (s *fakeStreamer) ChatStream(ctx context.Context, messages []llm.Message, o
return nil
}
-// small JSON writer for tests
-func writeJSON(t *testing.T, path string, v any) {
+// small TOML writer for tests (string values only)
+func writeTOML(t *testing.T, path string, m map[string]string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
@@ -73,8 +72,10 @@ func writeJSON(t *testing.T, path string, v any) {
t.Fatalf("create: %v", err)
}
defer f.Close()
- if err := json.NewEncoder(f).Encode(v); err != nil {
- t.Fatalf("encode: %v", err)
+ for k, v := range m {
+ if _, err := f.WriteString(k + " = \"" + v + "\"\n"); err != nil {
+ t.Fatalf("write: %v", err)
+ }
}
}