Detailed changes
@@ -151,7 +151,7 @@ func (c *coordinator) agenticFetchTool(_ context.Context, client *http.Client) (
return fantasy.ToolResponse{}, fmt.Errorf("error building models: %s", err)
}
- systemPrompt, err := promptTemplate.Build(ctx, small.Model.Provider(), small.Model.Model(), *c.cfg)
+ systemPrompt, err := promptTemplate.Build(ctx, small.Model.Provider(), small.Model.Model(), c.cfgSvc)
if err != nil {
return fantasy.ToolResponse{}, fmt.Errorf("error building system prompt: %s", err)
}
@@ -190,7 +190,7 @@ func coderAgent(r *vcr.Recorder, env fakeEnv, large, small fantasy.LanguageModel
// would be included in prompt and break VCR cassette matching.
cfg.Config().LSP = nil
- systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), *cfg.Config())
+ systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), cfg)
if err != nil {
return nil, err
}
@@ -375,7 +375,7 @@ func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, age
})
c.readyWg.Go(func() error {
- systemPrompt, err := prompt.Build(ctx, large.Model.Provider(), large.Model.Model(), *c.cfg)
+ systemPrompt, err := prompt.Build(ctx, large.Model.Provider(), large.Model.Model(), c.cfgSvc)
if err != nil {
return err
}
@@ -449,7 +449,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
}
}
- for _, tool := range tools.GetMCPTools(c.permissions, c.cfg, c.cfgSvc.WorkingDir()) {
+ for _, tool := range tools.GetMCPTools(c.permissions, c.cfgSvc, c.cfgSvc.WorkingDir()) {
if agent.AllowedMCP == nil {
// No MCP restrictions
filteredTools = append(filteredTools, tool)
@@ -29,7 +29,7 @@ type Prompt struct {
type PromptDat struct {
Provider string
Model string
- Config config.Config
+ Config promptConfig
WorkingDir string
IsGitRepo bool
Platform string
@@ -39,6 +39,10 @@ type PromptDat struct {
AvailSkillXML string
}
+type promptConfig struct {
+ LSP config.LSPs
+}
+
type ContextFile struct {
Path string
Content string
@@ -76,13 +80,13 @@ func NewPrompt(name, promptTemplate string, opts ...Option) (*Prompt, error) {
return p, nil
}
-func (p *Prompt) Build(ctx context.Context, provider, model string, cfg config.Config) (string, error) {
+func (p *Prompt) Build(ctx context.Context, provider, model string, svc *config.Service) (string, error) {
t, err := template.New(p.name).Parse(p.template)
if err != nil {
return "", fmt.Errorf("parsing template: %w", err)
}
var sb strings.Builder
- d, err := p.promptData(ctx, provider, model, cfg)
+ d, err := p.promptData(ctx, provider, model, svc)
if err != nil {
return "", err
}
@@ -104,11 +108,11 @@ func processFile(filePath string) *ContextFile {
}
}
-func processContextPath(p string, cfg config.Config) []ContextFile {
+func processContextPath(p string, workingDir string) []ContextFile {
var contexts []ContextFile
fullPath := p
if !filepath.IsAbs(p) {
- fullPath = filepath.Join(cfg.WorkingDir(), p)
+ fullPath = filepath.Join(workingDir, p)
}
info, err := os.Stat(fullPath)
if err != nil {
@@ -136,51 +140,51 @@ func processContextPath(p string, cfg config.Config) []ContextFile {
}
// expandPath expands ~ and environment variables in file paths
-func expandPath(path string, cfg config.Config) string {
+func expandPath(path string, resolver config.VariableResolver) string {
path = home.Long(path)
- // Handle environment variable expansion using the same pattern as config
if strings.HasPrefix(path, "$") {
- if expanded, err := cfg.Resolver().ResolveValue(path); err == nil {
- path = expanded
+ if resolver != nil {
+ if expanded, err := resolver.ResolveValue(path); err == nil {
+ path = expanded
+ }
}
}
return path
}
-func (p *Prompt) promptData(ctx context.Context, provider, model string, cfg config.Config) (PromptDat, error) {
- workingDir := cmp.Or(p.workingDir, cfg.WorkingDir())
+func (p *Prompt) promptData(ctx context.Context, provider, model string, svc *config.Service) (PromptDat, error) {
+ workingDir := cmp.Or(p.workingDir, svc.WorkingDir())
platform := cmp.Or(p.platform, runtime.GOOS)
files := map[string][]ContextFile{}
- for _, pth := range cfg.Options.ContextPaths {
- expanded := expandPath(pth, cfg)
+ for _, pth := range svc.ContextPaths() {
+ expanded := expandPath(pth, svc.Resolver())
pathKey := strings.ToLower(expanded)
if _, ok := files[pathKey]; ok {
continue
}
- content := processContextPath(expanded, cfg)
+ content := processContextPath(expanded, svc.WorkingDir())
files[pathKey] = content
}
- // Discover and load skills metadata.
var availSkillXML string
- if len(cfg.Options.SkillsPaths) > 0 {
- expandedPaths := make([]string, 0, len(cfg.Options.SkillsPaths))
- for _, pth := range cfg.Options.SkillsPaths {
- expandedPaths = append(expandedPaths, expandPath(pth, cfg))
+ if len(svc.SkillsPaths()) > 0 {
+ expandedPaths := make([]string, 0, len(svc.SkillsPaths()))
+ for _, pth := range svc.SkillsPaths() {
+ expandedPaths = append(expandedPaths, expandPath(pth, svc.Resolver()))
}
if discoveredSkills := skills.Discover(expandedPaths); len(discoveredSkills) > 0 {
availSkillXML = skills.ToPromptXML(discoveredSkills)
}
}
- isGit := isGitRepo(cfg.WorkingDir())
+ isGit := isGitRepo(svc.WorkingDir())
data := PromptDat{
Provider: provider,
Model: model,
- Config: cfg,
+ Config: promptConfig{LSP: svc.LSP()},
WorkingDir: filepath.ToSlash(workingDir),
IsGitRepo: isGit,
Platform: platform,
@@ -189,7 +193,7 @@ func (p *Prompt) promptData(ctx context.Context, provider, model string, cfg con
}
if isGit {
var err error
- data.GitStatus, err = getGitStatus(ctx, cfg.WorkingDir())
+ data.GitStatus, err = getGitStatus(ctx, svc.WorkingDir())
if err != nil {
return PromptDat{}, err
}
@@ -33,10 +33,10 @@ func taskPrompt(opts ...prompt.Option) (*prompt.Prompt, error) {
return systemPrompt, nil
}
-func InitializePrompt(cfg config.Config) (string, error) {
+func InitializePrompt(svc *config.Service) (string, error) {
systemPrompt, err := prompt.NewPrompt("initialize", string(initializePromptTmpl))
if err != nil {
return "", err
}
- return systemPrompt.Build(context.Background(), "", "", cfg)
+ return systemPrompt.Build(context.Background(), "", "", svc)
}
@@ -11,7 +11,7 @@ import (
)
// GetMCPTools gets all the currently available MCP tools.
-func GetMCPTools(permissions permission.Service, cfg *config.Config, wd string) []*Tool {
+func GetMCPTools(permissions permission.Service, cfg *config.Service, wd string) []*Tool {
var result []*Tool
for mcpName, tools := range mcp.Tools() {
for _, tool := range tools {
@@ -31,7 +31,7 @@ func GetMCPTools(permissions permission.Service, cfg *config.Config, wd string)
type Tool struct {
mcpName string
tool *mcp.Tool
- cfg *config.Config
+ cfg *config.Service
permissions permission.Service
workingDir string
providerOptions fantasy.ProviderOptions
@@ -134,11 +134,11 @@ func Close() error {
}
// Initialize initializes MCP clients based on the provided configuration.
-func Initialize(ctx context.Context, permissions permission.Service, cfg *config.Config) {
+func Initialize(ctx context.Context, permissions permission.Service, cfg *config.Service) {
slog.Info("Initializing MCP clients")
var wg sync.WaitGroup
// Initialize states for all configured MCPs
- for name, m := range cfg.MCP {
+ for name, m := range cfg.MCP() {
if m.Disabled {
updateState(name, StateDisabled, nil, nil, Counts{})
slog.Debug("Skipping disabled MCP", "name", name)
@@ -214,13 +214,13 @@ func WaitForInit(ctx context.Context) error {
}
}
-func getOrRenewClient(ctx context.Context, cfg *config.Config, name string) (*mcp.ClientSession, error) {
+func getOrRenewClient(ctx context.Context, cfg *config.Service, name string) (*mcp.ClientSession, error) {
sess, ok := sessions.Get(name)
if !ok {
return nil, fmt.Errorf("mcp '%s' not available", name)
}
- m := cfg.MCP[name]
+ m := cfg.MCP()[name]
state, _ := states.Get(name)
timeout := mcpTimeout(m)
@@ -20,7 +20,7 @@ func Prompts() iter.Seq2[string, []*Prompt] {
}
// GetPromptMessages retrieves the content of an MCP prompt with the given arguments.
-func GetPromptMessages(ctx context.Context, cfg *config.Config, clientName, promptName string, args map[string]string) ([]string, error) {
+func GetPromptMessages(ctx context.Context, cfg *config.Service, clientName, promptName string, args map[string]string) ([]string, error) {
c, err := getOrRenewClient(ctx, cfg, clientName)
if err != nil {
return nil, err
@@ -32,7 +32,7 @@ func Tools() iter.Seq2[string, []*Tool] {
}
// RunTool runs an MCP tool with the given input parameters.
-func RunTool(ctx context.Context, cfg *config.Config, name, toolName string, input string) (ToolResult, error) {
+func RunTool(ctx context.Context, cfg *config.Service, name, toolName string, input string) (ToolResult, error) {
var args map[string]any
if err := json.Unmarshal([]byte(input), &args); err != nil {
return ToolResult{}, fmt.Errorf("error parsing parameters: %s", err)
@@ -108,7 +108,7 @@ func RunTool(ctx context.Context, cfg *config.Config, name, toolName string, inp
// RefreshTools gets the updated list of tools from the MCP and updates the
// global state.
-func RefreshTools(ctx context.Context, cfg *config.Config, name string) {
+func RefreshTools(ctx context.Context, cfg *config.Service, name string) {
session, ok := sessions.Get(name)
if !ok {
slog.Warn("Refresh tools: no session", "name", name)
@@ -139,7 +139,7 @@ func getTools(ctx context.Context, session *mcp.ClientSession) ([]*Tool, error)
return result.Tools, nil
}
-func updateTools(cfg *config.Config, name string, tools []*Tool) int {
+func updateTools(cfg *config.Service, name string, tools []*Tool) int {
tools = filterDisabledTools(cfg, name, tools)
if len(tools) == 0 {
allTools.Del(name)
@@ -150,8 +150,8 @@ func updateTools(cfg *config.Config, name string, tools []*Tool) int {
}
// filterDisabledTools removes tools that are disabled via config.
-func filterDisabledTools(cfg *config.Config, mcpName string, tools []*Tool) []*Tool {
- mcpCfg, ok := cfg.MCP[mcpName]
+func filterDisabledTools(cfg *config.Service, mcpName string, tools []*Tool) []*Tool {
+ mcpCfg, ok := cfg.MCP()[mcpName]
if !ok || len(mcpCfg.DisabledTools) == 0 {
return tools
}
@@ -110,7 +110,7 @@ func New(ctx context.Context, conn *sql.DB, cfgSvc *config.Service) (*App, error
// Check for updates in the background.
go app.checkForUpdates(ctx)
- go mcp.Initialize(ctx, app.Permissions, cfg)
+ go mcp.Initialize(ctx, app.Permissions, cfgSvc)
// cleanup database upon app shutdown
app.cleanupFuncs = append(app.cleanupFuncs, conn.Close, mcp.Close)
@@ -53,7 +53,7 @@ type commandSource struct {
// LoadCustomCommands loads custom commands from multiple sources including
// XDG config directory, home directory, and project directory.
-func LoadCustomCommands(cfg *config.Config) ([]CustomCommand, error) {
+func LoadCustomCommands(cfg *config.Service) ([]CustomCommand, error) {
return loadAll(buildCommandSources(cfg))
}
@@ -89,7 +89,7 @@ func LoadMCPPrompts() ([]MCPPrompt, error) {
return commands, nil
}
-func buildCommandSources(cfg *config.Config) []commandSource {
+func buildCommandSources(cfg *config.Service) []commandSource {
var sources []commandSource
// XDG config directory
@@ -110,7 +110,7 @@ func buildCommandSources(cfg *config.Config) []commandSource {
// Project directory
sources = append(sources, commandSource{
- path: filepath.Join(cfg.Options.DataDirectory, "commands"),
+ path: filepath.Join(cfg.DataDirectory(), "commands"),
prefix: projectCommandPrefix,
})
@@ -227,7 +227,7 @@ func isMarkdownFile(name string) bool {
return strings.HasSuffix(strings.ToLower(name), ".md")
}
-func GetMCPPrompt(cfg *config.Config, clientID, promptID string, args map[string]string) (string, error) {
+func GetMCPPrompt(cfg *config.Service, clientID, promptID string, args map[string]string) (string, error) {
// TODO: we should pass the context down
result, err := mcp.GetPromptMessages(context.Background(), cfg, clientID, promptID, args)
if err != nil {
@@ -379,16 +379,6 @@ type Config struct {
Tools Tools `json:"tools,omitempty" jsonschema:"description=Tool configurations"`
Agents map[string]Agent `json:"-"`
-
- // Internal
- workingDir string `json:"-"`
- // TODO: find a better way to do this this should probably not be part of the config
- resolver VariableResolver
- knownProviders []catwalk.Provider `json:"-"`
-}
-
-func (c *Config) WorkingDir() string {
- return c.workingDir
}
func (c *Config) EnabledProviders() []ProviderConfig {
@@ -452,13 +442,6 @@ func (c *Config) SmallModel() *catwalk.Model {
return c.GetModel(model.Provider, model.Model)
}
-func (c *Config) Resolve(key string) (string, error) {
- if c.resolver == nil {
- return "", fmt.Errorf("no variable resolver configured")
- }
- return c.resolver.ResolveValue(key)
-}
-
func allToolNames() []string {
return []string{
"agent",
@@ -509,10 +492,6 @@ func filterSlice(data []string, mask []string, include bool) []string {
return filtered
}
-func (c *Config) Resolver() VariableResolver {
- return c.resolver
-}
-
func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
testURL := ""
headers := make(map[string]string)
@@ -26,12 +26,12 @@ func Init(workingDir, dataDir string, debug bool) (*Service, error) {
return svc, nil
}
-func ProjectNeedsInitialization(cfg *Config) (bool, error) {
- if cfg == nil {
+func ProjectNeedsInitialization(svc *Service) (bool, error) {
+ if svc == nil {
return false, fmt.Errorf("config not loaded")
}
- flagFilePath := filepath.Join(cfg.Options.DataDirectory, InitFlagFilename)
+ flagFilePath := filepath.Join(svc.DataDirectory(), InitFlagFilename)
_, err := os.Stat(flagFilePath)
if err == nil {
@@ -42,7 +42,7 @@ func ProjectNeedsInitialization(cfg *Config) (bool, error) {
return false, fmt.Errorf("failed to check init flag file: %w", err)
}
- someContextFileExists, err := contextPathsExist(cfg.WorkingDir())
+ someContextFileExists, err := contextPathsExist(svc.WorkingDir())
if err != nil {
return false, fmt.Errorf("failed to check for context files: %w", err)
}
@@ -50,8 +50,7 @@ func ProjectNeedsInitialization(cfg *Config) (bool, error) {
return false, nil
}
- // If the working directory has no non-ignored files, skip initialization step
- empty, err := dirHasNoVisibleFiles(cfg.WorkingDir())
+ empty, err := dirHasNoVisibleFiles(svc.WorkingDir())
if err != nil {
return false, fmt.Errorf("failed to check if directory is empty: %w", err)
}
@@ -99,11 +98,11 @@ func dirHasNoVisibleFiles(dir string) (bool, error) {
return len(files) == 0, nil
}
-func MarkProjectInitialized(cfg *Config) error {
- if cfg == nil {
+func MarkProjectInitialized(svc *Service) error {
+ if svc == nil {
return fmt.Errorf("config not loaded")
}
- flagFilePath := filepath.Join(cfg.Options.DataDirectory, InitFlagFilename)
+ flagFilePath := filepath.Join(svc.DataDirectory(), InitFlagFilename)
file, err := os.Create(flagFilePath)
if err != nil {
@@ -114,13 +113,13 @@ func MarkProjectInitialized(cfg *Config) error {
return nil
}
-func HasInitialDataConfig(cfg *Config) bool {
- if cfg == nil {
+func HasInitialDataConfig(svc *Service) bool {
+ if svc == nil {
return false
}
cfgPath := GlobalConfigData()
if _, err := os.Stat(cfgPath); err != nil {
return false
}
- return cfg.IsConfigured()
+ return svc.IsConfigured()
}
@@ -79,13 +79,10 @@ func Load(workingDir, dataDir string, debug bool) (*Service, error) {
return nil, err
}
svc.knownProviders = providers
- cfg.knownProviders = providers
env := env.New()
- // Configure providers
valueResolver := NewShellVariableResolver(env)
svc.resolver = valueResolver
- cfg.resolver = valueResolver
if err := svc.configureProviders(env, valueResolver, svc.knownProviders); err != nil {
return nil, fmt.Errorf("failed to configure providers: %w", err)
}
@@ -344,7 +341,6 @@ func (s *Service) configureProviders(env env.Env, resolver VariableResolver, kno
}
func (c *Config) setDefaults(workingDir, dataDir string) {
- c.workingDir = workingDir
if c.Options == nil {
c.Options = &Options{}
}
@@ -57,7 +57,6 @@ func TestConfig_setDefaults(t *testing.T) {
for _, path := range defaultContextPaths {
require.Contains(t, cfg.Options.ContextPaths, path)
}
- require.Equal(t, "/tmp", cfg.workingDir)
}
func TestConfig_configureProviders(t *testing.T) {
@@ -19,7 +19,7 @@ import (
// markProjectInitialized marks the current project as initialized in the config.
func (m *UI) markProjectInitialized() tea.Msg {
// TODO: handle error so we show it in the tui footer
- err := config.MarkProjectInitialized(m.com.ConfigService().Config())
+ err := config.MarkProjectInitialized(m.com.ConfigService())
if err != nil {
slog.Error(err.Error())
}
@@ -52,10 +52,10 @@ func (m *UI) initializeProject() tea.Cmd {
if cmd := m.newSession(); cmd != nil {
cmds = append(cmds, cmd)
}
- cfg := m.com.ConfigService().Config()
+ svc := m.com.ConfigService()
initialize := func() tea.Msg {
- initPrompt, err := agent.InitializePrompt(*cfg)
+ initPrompt, err := agent.InitializePrompt(svc)
if err != nil {
return util.InfoMsg{Type: util.InfoTypeError, Msg: err.Error()}
}
@@ -298,7 +298,7 @@ func New(com *common.Common) *UI {
desiredFocus := uiFocusEditor
if !com.ConfigService().IsConfigured() {
desiredState = uiOnboarding
- } else if n, _ := config.ProjectNeedsInitialization(com.ConfigService().Config()); n {
+ } else if n, _ := config.ProjectNeedsInitialization(com.ConfigService()); n {
desiredState = uiInitialize
}
@@ -345,7 +345,7 @@ func (m *UI) setState(state uiState, focus uiFocusState) {
// loadCustomCommands loads the custom commands asynchronously.
func (m *UI) loadCustomCommands() tea.Cmd {
return func() tea.Msg {
- customCommands, err := commands.LoadCustomCommands(m.com.ConfigService().Config())
+ customCommands, err := commands.LoadCustomCommands(m.com.ConfigService())
if err != nil {
slog.Error("Failed to load custom commands", "error", err)
}
@@ -3063,7 +3063,7 @@ func (m *UI) drawSessionDetails(scr uv.Screen, area uv.Rectangle) {
func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string) tea.Cmd {
load := func() tea.Msg {
- prompt, err := commands.GetMCPPrompt(m.com.ConfigService().Config(), clientID, promptID, arguments)
+ prompt, err := commands.GetMCPPrompt(m.com.ConfigService(), clientID, promptID, arguments)
if err != nil {
// TODO: make this better
return util.ReportError(err)()