From c3c71345db9086392cd9b7529c7f5287009c226e Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 24 Sep 2025 23:21:43 +0300 Subject: Add runtime config store and reload command --- internal/hexailsp/run.go | 23 +++++++++++++++- internal/hexailsp/run_more_test.go | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) (limited to 'internal/hexailsp') diff --git a/internal/hexailsp/run.go b/internal/hexailsp/run.go index 554e604..ffb9f86 100644 --- a/internal/hexailsp/run.go +++ b/internal/hexailsp/run.go @@ -13,6 +13,7 @@ import ( "codeberg.org/snonux/hexai/internal/llm" "codeberg.org/snonux/hexai/internal/logging" "codeberg.org/snonux/hexai/internal/lsp" + "codeberg.org/snonux/hexai/internal/runtimeconfig" "codeberg.org/snonux/hexai/internal/stats" ) @@ -55,8 +56,26 @@ func RunWithFactory(logPath string, stdin io.Reader, stdout io.Writer, logger *l client = buildClientIfNil(cfg, client) factory = ensureFactory(factory) - opts := makeServerOptions(cfg, strings.TrimSpace(logPath) != "", client) + store := runtimeconfig.New(cfg) + logContext := strings.TrimSpace(logPath) != "" + opts := makeServerOptions(cfg, logContext, client) + opts.ConfigStore = store server := factory(stdin, stdout, logger, opts) + if configurable, ok := server.(interface{ ApplyOptions(lsp.ServerOptions) }); ok { + store.Subscribe(func(oldCfg, newCfg appconfig.App) { + updated := newCfg + normalizeLoggingConfig(&updated) + if updated.StatsWindowMinutes > 0 { + stats.SetWindow(time.Duration(updated.StatsWindowMinutes) * time.Minute) + } + if newClient := buildClientIfNil(updated, nil); newClient != nil { + client = newClient + } + opts := makeServerOptions(updated, logContext, client) + opts.ConfigStore = store + configurable.ApplyOptions(opts) + }) + } if err := server.Run(); err != nil { logger.Fatalf("server error: %v", err) } @@ -135,6 +154,8 @@ func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) ls } return lsp.ServerOptions{ LogContext: logContext, + ConfigStore: nil, + Config: &cfg, MaxTokens: cfg.MaxTokens, ContextMode: cfg.ContextMode, WindowLines: cfg.ContextWindowLines, diff --git a/internal/hexailsp/run_more_test.go b/internal/hexailsp/run_more_test.go index 00b79c1..faaae41 100644 --- a/internal/hexailsp/run_more_test.go +++ b/internal/hexailsp/run_more_test.go @@ -2,18 +2,34 @@ package hexailsp import ( "bytes" + "context" "io" "log" "testing" "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" "codeberg.org/snonux/hexai/internal/lsp" + "codeberg.org/snonux/hexai/internal/runtimeconfig" ) type recRunner struct{ ran bool } func (r *recRunner) Run() error { r.ran = true; return nil } +type applyRunner struct{ opts []lsp.ServerOptions } + +func (r *applyRunner) Run() error { return nil } +func (r *applyRunner) ApplyOptions(opts lsp.ServerOptions) { r.opts = append(r.opts, opts) } + +type stubClient struct{} + +func (stubClient) Chat(context.Context, []llm.Message, ...llm.RequestOption) (string, error) { + return "", nil +} +func (stubClient) Name() string { return "stub" } +func (stubClient) DefaultModel() string { return "stub-model" } + func TestRunWithFactory_BuildsOptionsAndClient(t *testing.T) { var captured lsp.ServerOptions factory := func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner { @@ -41,3 +57,41 @@ func TestRunWithFactory_BuildsOptionsAndClient(t *testing.T) { t.Fatalf("expected client to be constructed") } } + +func TestRunWithFactory_SubscriptionAppliesUpdates(t *testing.T) { + var in, out bytes.Buffer + logger := log.New(io.Discard, "", 0) + runner := &applyRunner{} + var capturedStore *runtimeconfig.Store + factory := func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner { + capturedStore = opts.ConfigStore + runner.opts = append(runner.opts, opts) + return runner + } + cfg := appconfig.Load(nil) + cfg.StatsWindowMinutes = 0 + cfg.ContextMode = " WINDOW " + if err := RunWithFactory("", &in, &out, logger, cfg, stubClient{}, factory); err != nil { + t.Fatalf("RunWithFactory error: %v", err) + } + if capturedStore == nil { + t.Fatal("expected config store to be passed to factory") + } + if len(runner.opts) == 0 { + t.Fatal("expected initial options to be recorded") + } + updated := cfg + updated.MaxTokens = cfg.MaxTokens + 10 + updated.ContextMode = "always-full" + capturedStore.Set(updated) + if len(runner.opts) < 2 { + t.Fatalf("expected ApplyOptions to be invoked on config update, got %d calls", len(runner.opts)) + } + latest := runner.opts[len(runner.opts)-1] + if latest.MaxTokens != updated.MaxTokens { + t.Fatalf("expected updated max tokens, got %+v", latest) + } + if latest.ContextMode != "always-full" { + t.Fatalf("expected normalized context mode, got %+v", latest) + } +} -- cgit v1.2.3