From 27727343e0ce15d0f29d7d575e88d8566ebaf566 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Sun, 5 Apr 2026 15:34:03 -0400 Subject: [PATCH] feat(tools): crush_info tool for readling live config --- internal/agent/coordinator.go | 1 + internal/config/config.go | 1 + internal/config/load.go | 16 ++++++++++++---- internal/config/load_bench_test.go | 6 +++--- internal/config/load_test.go | 4 ++-- internal/config/store.go | 18 ++++++++++++++++-- internal/lsp/client.go | 5 +++++ 7 files changed, 40 insertions(+), 11 deletions(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 59401896d00c46c4fca25d423687969bd5d5191a..2b599cb3705b633dfb7f30688f04593292df0414 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -447,6 +447,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan allTools = append(allTools, tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Config().Options.Attribution, modelName), + tools.NewCrushInfoTool(c.cfg, c.lspManager), tools.NewJobOutputTool(), tools.NewJobKillTool(), tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil), diff --git a/internal/config/config.go b/internal/config/config.go index 71a517a5398151f346998b4c3ff1d91afadd4ee1..457413b3f32a25b23934681c527286ad6d090375 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -462,6 +462,7 @@ func allToolNames() []string { return []string{ "agent", "bash", + "crush_info", "job_output", "job_kill", "download", diff --git a/internal/config/load.go b/internal/config/load.go index 8f1ea635d36881da660dbd8df0db4c73a77fc50f..2de15c1068c576cd24867995790a02a8c9cba533 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -33,7 +33,7 @@ const defaultCatwalkURL = "https://catwalk.charm.sh" func Load(workingDir, dataDir string, debug bool) (*ConfigStore, error) { configPaths := lookupConfigs(workingDir) - cfg, err := loadFromConfigPaths(configPaths) + cfg, loadedPaths, err := loadFromConfigPaths(configPaths) if err != nil { return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err) } @@ -45,6 +45,7 @@ func Load(workingDir, dataDir string, debug bool) (*ConfigStore, error) { workingDir: workingDir, globalDataPath: GlobalConfigData(), workspacePath: filepath.Join(cfg.Options.DataDirectory, fmt.Sprintf("%s.json", appName)), + loadedPaths: loadedPaths, } if debug { @@ -60,6 +61,7 @@ func Load(workingDir, dataDir string, debug bool) (*ConfigStore, error) { *cfg = *merged cfg.setDefaults(workingDir, dataDir) store.config = cfg + store.loadedPaths = append(store.loadedPaths, store.workspacePath) } } @@ -669,8 +671,9 @@ func lookupConfigs(cwd string) []string { return append(configPaths, foundConfigs...) } -func loadFromConfigPaths(configPaths []string) (*Config, error) { +func loadFromConfigPaths(configPaths []string) (*Config, []string, error) { var configs [][]byte + var loaded []string for _, path := range configPaths { data, err := os.ReadFile(path) @@ -678,15 +681,20 @@ func loadFromConfigPaths(configPaths []string) (*Config, error) { if os.IsNotExist(err) { continue } - return nil, fmt.Errorf("failed to open config file %s: %w", path, err) + return nil, nil, fmt.Errorf("failed to open config file %s: %w", path, err) } if len(data) == 0 { continue } configs = append(configs, data) + loaded = append(loaded, path) } - return loadFromBytes(configs) + cfg, err := loadFromBytes(configs) + if err != nil { + return nil, nil, err + } + return cfg, loaded, nil } func loadFromBytes(configs [][]byte) (*Config, error) { diff --git a/internal/config/load_bench_test.go b/internal/config/load_bench_test.go index 3df43946d438fd478b63a1dfd242ee4c35e71896..752f0925f59a5e890666fc756b0b5c4f41ea4394 100644 --- a/internal/config/load_bench_test.go +++ b/internal/config/load_bench_test.go @@ -53,7 +53,7 @@ func BenchmarkLoadFromConfigPaths(b *testing.B) { b.ReportAllocs() for b.Loop() { - _, err := loadFromConfigPaths(configPaths) + _, _, err := loadFromConfigPaths(configPaths) if err != nil { b.Fatal(err) } @@ -78,7 +78,7 @@ func BenchmarkLoadFromConfigPaths_MissingFiles(b *testing.B) { b.ReportAllocs() for b.Loop() { - _, err := loadFromConfigPaths(configPaths) + _, _, err := loadFromConfigPaths(configPaths) if err != nil { b.Fatal(err) } @@ -95,7 +95,7 @@ func BenchmarkLoadFromConfigPaths_Empty(b *testing.B) { b.ReportAllocs() for b.Loop() { - _, err := loadFromConfigPaths(configPaths) + _, _, err := loadFromConfigPaths(configPaths) if err != nil { b.Fatal(err) } diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 62b1eaa2437116b0a051fe10183689650d388472..16ff61414011cc24c8153d0bba7352d741de3ddb 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -490,7 +490,7 @@ func TestConfig_setupAgentsWithDisabledTools(t *testing.T) { coderAgent, ok := cfg.Agents[AgentCoder] require.True(t, ok) - assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "todos", "view", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools) + assert.Equal(t, []string{"agent", "bash", "crush_info", "job_output", "job_kill", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "todos", "view", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools) taskAgent, ok := cfg.Agents[AgentTask] require.True(t, ok) @@ -513,7 +513,7 @@ func TestConfig_setupAgentsWithEveryReadOnlyToolDisabled(t *testing.T) { cfg.SetupAgents() coderAgent, ok := cfg.Agents[AgentCoder] require.True(t, ok) - assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "todos", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools) + assert.Equal(t, []string{"agent", "bash", "crush_info", "job_output", "job_kill", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "todos", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools) taskAgent, ok := cfg.Agents[AgentTask] require.True(t, ok) diff --git a/internal/config/store.go b/internal/config/store.go index 7df0d4f27f5c0c4d15c45e0f92ce4128fc829c38..ab38e013c45c52ccc4335014162f2d0c4e183b61 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -32,8 +32,9 @@ type ConfigStore struct { config *Config workingDir string resolver VariableResolver - globalDataPath string // ~/.local/share/crush/crush.json - workspacePath string // .crush/crush.json + globalDataPath string // ~/.local/share/crush/crush.json + workspacePath string // .crush/crush.json + loadedPaths []string // config files that were successfully loaded knownProviders []catwalk.Provider overrides RuntimeOverrides } @@ -76,6 +77,11 @@ func (s *ConfigStore) Overrides() *RuntimeOverrides { return &s.overrides } +// LoadedPaths returns the config file paths that were successfully loaded. +func (s *ConfigStore) LoadedPaths() []string { + return slices.Clone(s.loadedPaths) +} + // configPath returns the file path for the given scope. func (s *ConfigStore) configPath(scope Scope) (string, error) { switch scope { @@ -337,6 +343,14 @@ func (s *ConfigStore) recordRecentModel(scope Scope, modelType SelectedModelType return nil } +// NewTestStore creates a ConfigStore for testing purposes. +func NewTestStore(cfg *Config, loadedPaths ...string) *ConfigStore { + return &ConfigStore{ + config: cfg, + loadedPaths: loadedPaths, + } +} + // ImportCopilot attempts to import a GitHub Copilot token from disk. func (s *ConfigStore) ImportCopilot() (*oauth.Token, bool) { if s.HasConfigField(ScopeGlobal, "providers.copilot.api_key") || s.HasConfigField(ScopeGlobal, "providers.copilot.oauth") { diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 2aa1b49a781489598f865cfd067d1adf5d68b7aa..7125b352b6c2d5e841c0687bdedd50a93cc1ae22 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -286,6 +286,11 @@ func (c *Client) GetName() string { return c.name } +// FileTypes returns the file types this LSP client handles +func (c *Client) FileTypes() []string { + return c.fileTypes +} + // SetDiagnosticsCallback sets the callback function for diagnostic changes func (c *Client) SetDiagnosticsCallback(callback func(name string, count int)) { c.onDiagnosticsChanged = callback