on main help sign in
refactor: remove global config (#2132)
2c9670aKujtim Hoxha
internal/agent/coordinator.go+1−1
@@ -446,7 +446,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
446 }
446 }
447 }
447 }
448
448
449− for _, tool := range tools.GetMCPTools(c.permissions, c.cfg.WorkingDir()) {
449+ for _, tool := range tools.GetMCPTools(c.permissions, c.cfg, c.cfg.WorkingDir()) {
450 if agent.AllowedMCP == nil {
450 if agent.AllowedMCP == nil {
451 // No MCP restrictions
451 // No MCP restrictions
452 filteredTools = append(filteredTools, tool)
452 filteredTools = append(filteredTools, tool)
internal/agent/tools/mcp-tools.go+5−2
@@ -6,11 +6,12 @@ import (
6
6
7 "charm.land/fantasy"
7 "charm.land/fantasy"
8 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
8 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
9+ "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/permission"
10 "github.com/charmbracelet/crush/internal/permission"
10)
11)
11
12
12// GetMCPTools gets all the currently available MCP tools.
13// GetMCPTools gets all the currently available MCP tools.
13−func GetMCPTools(permissions permission.Service, wd string) []*Tool {
14+func GetMCPTools(permissions permission.Service, cfg *config.Config, wd string) []*Tool {
14 var result []*Tool
15 var result []*Tool
15 for mcpName, tools := range mcp.Tools() {
16 for mcpName, tools := range mcp.Tools() {
16 for _, tool := range tools {
17 for _, tool := range tools {
@@ -19,6 +20,7 @@ func GetMCPTools(permissions permission.Service, wd string) []*Tool {
19 tool: tool,
20 tool: tool,
20 permissions: permissions,
21 permissions: permissions,
21 workingDir: wd,
22 workingDir: wd,
23+ cfg: cfg,
22 })
24 })
23 }
25 }
24 }
26 }
@@ -29,6 +31,7 @@ func GetMCPTools(permissions permission.Service, wd string) []*Tool {
29type Tool struct {
31type Tool struct {
30 mcpName string
32 mcpName string
31 tool *mcp.Tool
33 tool *mcp.Tool
34+ cfg *config.Config
32 permissions permission.Service
35 permissions permission.Service
33 workingDir string
36 workingDir string
34 providerOptions fantasy.ProviderOptions
37 providerOptions fantasy.ProviderOptions
@@ -107,7 +110,7 @@ func (m *Tool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolRe
107 return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
110 return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
108 }
111 }
109
112
110− result, err := mcp.RunTool(ctx, m.mcpName, m.tool.Name, params.Input)
113+ result, err := mcp.RunTool(ctx, m.cfg, m.mcpName, m.tool.Name, params.Input)
111 if err != nil {
114 if err != nil {
112 return fantasy.NewTextErrorResponse(err.Error()), nil
115 return fantasy.NewTextErrorResponse(err.Error()), nil
113 }
116 }
internal/agent/tools/mcp/init.go+2−3
@@ -189,7 +189,7 @@ func Initialize(ctx context.Context, permissions permission.Service, cfg *config
189 return
189 return
190 }
190 }
191
191
192− toolCount := updateTools(name, tools)
192+ toolCount := updateTools(cfg, name, tools)
193 updatePrompts(name, prompts)
193 updatePrompts(name, prompts)
194 sessions.Set(name, session)
194 sessions.Set(name, session)
195
195
@@ -214,13 +214,12 @@ func WaitForInit(ctx context.Context) error {
214 }
214 }
215}
215}
216
216
217−func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
217+func getOrRenewClient(ctx context.Context, cfg *config.Config, name string) (*mcp.ClientSession, error) {
218 sess, ok := sessions.Get(name)
218 sess, ok := sessions.Get(name)
219 if !ok {
219 if !ok {
220 return nil, fmt.Errorf("mcp '%s' not available", name)
220 return nil, fmt.Errorf("mcp '%s' not available", name)
221 }
221 }
222
222
223− cfg := config.Get()
224 m := cfg.MCP[name]
223 m := cfg.MCP[name]
225 state, _ := states.Get(name)
224 state, _ := states.Get(name)
226
225
internal/agent/tools/mcp/prompts.go+3−2
@@ -5,6 +5,7 @@ import (
5 "iter"
5 "iter"
6 "log/slog"
6 "log/slog"
7
7
8+ "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/csync"
9 "github.com/charmbracelet/crush/internal/csync"
9 "github.com/modelcontextprotocol/go-sdk/mcp"
10 "github.com/modelcontextprotocol/go-sdk/mcp"
10)
11)
@@ -19,8 +20,8 @@ func Prompts() iter.Seq2[string, []*Prompt] {
19}
20}
20
21
21// GetPromptMessages retrieves the content of an MCP prompt with the given arguments.
22// GetPromptMessages retrieves the content of an MCP prompt with the given arguments.
22−func GetPromptMessages(ctx context.Context, clientName, promptName string, args map[string]string) ([]string, error) {
23+func GetPromptMessages(ctx context.Context, cfg *config.Config, clientName, promptName string, args map[string]string) ([]string, error) {
23− c, err := getOrRenewClient(ctx, clientName)
24+ c, err := getOrRenewClient(ctx, cfg, clientName)
24 if err != nil {
25 if err != nil {
25 return nil, err
26 return nil, err
26 }
27 }
internal/agent/tools/mcp/tools.go+7−8
@@ -32,13 +32,13 @@ func Tools() iter.Seq2[string, []*Tool] {
32}
32}
33
33
34// RunTool runs an MCP tool with the given input parameters.
34// RunTool runs an MCP tool with the given input parameters.
35−func RunTool(ctx context.Context, name, toolName string, input string) (ToolResult, error) {
35+func RunTool(ctx context.Context, cfg *config.Config, name, toolName string, input string) (ToolResult, error) {
36 var args map[string]any
36 var args map[string]any
37 if err := json.Unmarshal([]byte(input), &args); err != nil {
37 if err := json.Unmarshal([]byte(input), &args); err != nil {
38 return ToolResult{}, fmt.Errorf("error parsing parameters: %s", err)
38 return ToolResult{}, fmt.Errorf("error parsing parameters: %s", err)
39 }
39 }
40
40
41− c, err := getOrRenewClient(ctx, name)
41+ c, err := getOrRenewClient(ctx, cfg, name)
42 if err != nil {
42 if err != nil {
43 return ToolResult{}, err
43 return ToolResult{}, err
44 }
44 }
@@ -108,7 +108,7 @@ func RunTool(ctx context.Context, name, toolName string, input string) (ToolResu
108
108
109// RefreshTools gets the updated list of tools from the MCP and updates the
109// RefreshTools gets the updated list of tools from the MCP and updates the
110// global state.
110// global state.
111−func RefreshTools(ctx context.Context, name string) {
111+func RefreshTools(ctx context.Context, cfg *config.Config, name string) {
112 session, ok := sessions.Get(name)
112 session, ok := sessions.Get(name)
113 if !ok {
113 if !ok {
114 slog.Warn("Refresh tools: no session", "name", name)
114 slog.Warn("Refresh tools: no session", "name", name)
@@ -121,7 +121,7 @@ func RefreshTools(ctx context.Context, name string) {
121 return
121 return
122 }
122 }
123
123
124− toolCount := updateTools(name, tools)
124+ toolCount := updateTools(cfg, name, tools)
125
125
126 prev, _ := states.Get(name)
126 prev, _ := states.Get(name)
127 prev.Counts.Tools = toolCount
127 prev.Counts.Tools = toolCount
@@ -139,8 +139,8 @@ func getTools(ctx context.Context, session *mcp.ClientSession) ([]*Tool, error)
139 return result.Tools, nil
139 return result.Tools, nil
140}
140}
141
141
142−func updateTools(name string, tools []*Tool) int {
142+func updateTools(cfg *config.Config, name string, tools []*Tool) int {
143− tools = filterDisabledTools(name, tools)
143+ tools = filterDisabledTools(cfg, name, tools)
144 if len(tools) == 0 {
144 if len(tools) == 0 {
145 allTools.Del(name)
145 allTools.Del(name)
146 return 0
146 return 0
@@ -150,8 +150,7 @@ func updateTools(name string, tools []*Tool) int {
150}
150}
151
151
152// filterDisabledTools removes tools that are disabled via config.
152// filterDisabledTools removes tools that are disabled via config.
153−func filterDisabledTools(mcpName string, tools []*Tool) []*Tool {
153+func filterDisabledTools(cfg *config.Config, mcpName string, tools []*Tool) []*Tool {
154− cfg := config.Get()
155 mcpCfg, ok := cfg.MCP[mcpName]
154 mcpCfg, ok := cfg.MCP[mcpName]
156 if !ok || len(mcpCfg.DisabledTools) == 0 {
155 if !ok || len(mcpCfg.DisabledTools) == 0 {
157 return tools
156 return tools
internal/app/lsp.go+1−1
@@ -114,7 +114,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
114 updateLSPState(name, lsp.StateStarting, nil, nil, 0)
114 updateLSPState(name, lsp.StateStarting, nil, nil, 0)
115
115
116 // Create LSP client.
116 // Create LSP client.
117− lspClient, err := lsp.New(ctx, name, config, app.config.Resolver())
117+ lspClient, err := lsp.New(ctx, name, config, app.config.Resolver(), app.config.Options.DebugLSP)
118 if err != nil {
118 if err != nil {
119 if !userConfigured {
119 if !userConfigured {
120 slog.Warn("Default LSP config skipped due to error", "name", name, "error", err)
120 slog.Warn("Default LSP config skipped due to error", "name", name, "error", err)
internal/cmd/login.go+4−6
@@ -52,17 +52,16 @@ crush login copilot
52 }
52 }
53 switch provider {
53 switch provider {
54 case "hyper":
54 case "hyper":
55− return loginHyper()
55+ return loginHyper(app.Config())
56 case "copilot", "github", "github-copilot":
56 case "copilot", "github", "github-copilot":
57− return loginCopilot()
57+ return loginCopilot(app.Config())
58 default:
58 default:
59 return fmt.Errorf("unknown platform: %s", args[0])
59 return fmt.Errorf("unknown platform: %s", args[0])
60 }
60 }
61 },
61 },
62}
62}
63
63
64−func loginHyper() error {
64+func loginHyper(cfg *config.Config) error {
65− cfg := config.Get()
66 if !hyperp.Enabled() {
65 if !hyperp.Enabled() {
67 return fmt.Errorf("hyper not enabled")
66 return fmt.Errorf("hyper not enabled")
68 }
67 }
@@ -124,10 +123,9 @@ func loginHyper() error {
124 return nil
123 return nil
125}
124}
126
125
127−func loginCopilot() error {
126+func loginCopilot(cfg *config.Config) error {
128 ctx := getLoginContext()
127 ctx := getLoginContext()
129
128
130− cfg := config.Get()
131 if cfg.HasConfigField("providers.copilot.oauth") {
129 if cfg.HasConfigField("providers.copilot.oauth") {
132 fmt.Println("You are already logged in to GitHub Copilot.")
130 fmt.Println("You are already logged in to GitHub Copilot.")
133 return nil
131 return nil
internal/cmd/root.go+3−3
@@ -227,21 +227,21 @@ func setupApp(cmd *cobra.Command) (*app.App, error) {
227 return nil, err
227 return nil, err
228 }
228 }
229
229
230− if shouldEnableMetrics() {
230+ if shouldEnableMetrics(cfg) {
231 event.Init()
231 event.Init()
232 }
232 }
233
233
234 return appInstance, nil
234 return appInstance, nil
235}
235}
236
236
237−func shouldEnableMetrics() bool {
237+func shouldEnableMetrics(cfg *config.Config) bool {
238 if v, _ := strconv.ParseBool(os.Getenv("CRUSH_DISABLE_METRICS")); v {
238 if v, _ := strconv.ParseBool(os.Getenv("CRUSH_DISABLE_METRICS")); v {
239 return false
239 return false
240 }
240 }
241 if v, _ := strconv.ParseBool(os.Getenv("DO_NOT_TRACK")); v {
241 if v, _ := strconv.ParseBool(os.Getenv("DO_NOT_TRACK")); v {
242 return false
242 return false
243 }
243 }
244− if config.Get().Options.DisableMetrics {
244+ if cfg.Options.DisableMetrics {
245 return false
245 return false
246 }
246 }
247 return true
247 return true
internal/commands/commands.go+2−2
@@ -227,9 +227,9 @@ func isMarkdownFile(name string) bool {
227 return strings.HasSuffix(strings.ToLower(name), ".md")
227 return strings.HasSuffix(strings.ToLower(name), ".md")
228}
228}
229
229
230−func GetMCPPrompt(clientID, promptID string, args map[string]string) (string, error) {
230+func GetMCPPrompt(cfg *config.Config, clientID, promptID string, args map[string]string) (string, error) {
231 // TODO: we should pass the context down
231 // TODO: we should pass the context down
232− result, err := mcp.GetPromptMessages(context.Background(), clientID, promptID, args)
232+ result, err := mcp.GetPromptMessages(context.Background(), cfg, clientID, promptID, args)
233 if err != nil {
233 if err != nil {
234 return "", err
234 return "", err
235 }
235 }
internal/config/init.go+8−17
@@ -6,7 +6,6 @@ import (
6 "path/filepath"
6 "path/filepath"
7 "slices"
7 "slices"
8 "strings"
8 "strings"
9− "sync/atomic"
10
9
11 "github.com/charmbracelet/crush/internal/fsext"
10 "github.com/charmbracelet/crush/internal/fsext"
12)
11)
@@ -19,25 +18,15 @@ type ProjectInitFlag struct {
19 Initialized bool `json:"initialized"`
18 Initialized bool `json:"initialized"`
20}
19}
21
20
22−// TODO: we need to remove the global config instance keeping it now just until everything is migrated
23−var instance atomic.Pointer[Config]
24−
25func Init(workingDir, dataDir string, debug bool) (*Config, error) {
21func Init(workingDir, dataDir string, debug bool) (*Config, error) {
26 cfg, err := Load(workingDir, dataDir, debug)
22 cfg, err := Load(workingDir, dataDir, debug)
27 if err != nil {
23 if err != nil {
28 return nil, err
24 return nil, err
29 }
25 }
30− instance.Store(cfg)
31− return instance.Load(), nil
26+ return cfg, nil
32−}
33−
34−func Get() *Config {
35− cfg := instance.Load()
36− return cfg
37}
27}
38
28
39−func ProjectNeedsInitialization() (bool, error) {
29+func ProjectNeedsInitialization(cfg *Config) (bool, error) {
40− cfg := Get()
41 if cfg == nil {
30 if cfg == nil {
42 return false, fmt.Errorf("config not loaded")
31 return false, fmt.Errorf("config not loaded")
43 }
32 }
@@ -110,8 +99,7 @@ func dirHasNoVisibleFiles(dir string) (bool, error) {
110 return len(files) == 0, nil
99 return len(files) == 0, nil
111}
100}
112
101
113−func MarkProjectInitialized() error {
102+func MarkProjectInitialized(cfg *Config) error {
114− cfg := Get()
115 if cfg == nil {
103 if cfg == nil {
116 return fmt.Errorf("config not loaded")
104 return fmt.Errorf("config not loaded")
117 }
105 }
@@ -126,10 +114,13 @@ func MarkProjectInitialized() error {
126 return nil
114 return nil
127}
115}
128
116
129−func HasInitialDataConfig() bool {
117+func HasInitialDataConfig(cfg *Config) bool {
118+ if cfg == nil {
119+ return false
120+ }
130 cfgPath := GlobalConfigData()
121 cfgPath := GlobalConfigData()
131 if _, err := os.Stat(cfgPath); err != nil {
122 if _, err := os.Stat(cfgPath); err != nil {
132 return false
123 return false
133 }
124 }
134− return Get().IsConfigured()
125+ return cfg.IsConfigured()
135}
126}
internal/lsp/client.go+12−10
@@ -35,6 +35,7 @@ type DiagnosticCounts struct {
35type Client struct {
35type Client struct {
36 client *powernap.Client
36 client *powernap.Client
37 name string
37 name string
38+ debug bool
38
39
39 // Working directory this LSP is scoped to.
40 // Working directory this LSP is scoped to.
40 workDir string
41 workDir string
@@ -68,7 +69,7 @@ type Client struct {
68}
69}
69
70
70// New creates a new LSP client using the powernap implementation.
71// New creates a new LSP client using the powernap implementation.
71−func New(ctx context.Context, name string, cfg config.LSPConfig, resolver config.VariableResolver) (*Client, error) {
72+func New(ctx context.Context, name string, cfg config.LSPConfig, resolver config.VariableResolver, debug bool) (*Client, error) {
72 client := &Client{
73 client := &Client{
73 name: name,
74 name: name,
74 fileTypes: cfg.FileTypes,
75 fileTypes: cfg.FileTypes,
@@ -76,6 +77,7 @@ func New(ctx context.Context, name string, cfg config.LSPConfig, resolver config
76 openFiles: csync.NewMap[string, *OpenFileInfo](),
77 openFiles: csync.NewMap[string, *OpenFileInfo](),
77 config: cfg,
78 config: cfg,
78 ctx: ctx,
79 ctx: ctx,
80+ debug: debug,
79 resolver: resolver,
81 resolver: resolver,
80 }
82 }
81 client.serverState.Store(StateStarting)
83 client.serverState.Store(StateStarting)
@@ -174,7 +176,11 @@ func (c *Client) registerHandlers() {
174 c.RegisterServerRequestHandler("workspace/applyEdit", HandleApplyEdit)
176 c.RegisterServerRequestHandler("workspace/applyEdit", HandleApplyEdit)
175 c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
177 c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
176 c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
178 c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
177− c.RegisterNotificationHandler("window/showMessage", HandleServerMessage)
179+ c.RegisterNotificationHandler("window/showMessage", func(ctx context.Context, method string, params json.RawMessage) {
180+ if c.debug {
181+ HandleServerMessage(ctx, method, params)
182+ }
183+ })
178 c.RegisterNotificationHandler("textDocument/publishDiagnostics", func(_ context.Context, _ string, params json.RawMessage) {
184 c.RegisterNotificationHandler("textDocument/publishDiagnostics", func(_ context.Context, _ string, params json.RawMessage) {
179 HandleDiagnostics(c, params)
185 HandleDiagnostics(c, params)
180 })
186 })
@@ -262,8 +268,6 @@ func (c *Client) SetDiagnosticsCallback(callback func(name string, count int)) {
262
268
263// WaitForServerReady waits for the server to be ready
269// WaitForServerReady waits for the server to be ready
264func (c *Client) WaitForServerReady(ctx context.Context) error {
270func (c *Client) WaitForServerReady(ctx context.Context) error {
265− cfg := config.Get()
266−
267 // Set initial state
271 // Set initial state
268 c.SetServerState(StateStarting)
272 c.SetServerState(StateStarting)
269
273
@@ -275,7 +279,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
275 ticker := time.NewTicker(500 * time.Millisecond)
279 ticker := time.NewTicker(500 * time.Millisecond)
276 defer ticker.Stop()
280 defer ticker.Stop()
277
281
278− if cfg != nil && cfg.Options.DebugLSP {
282+ if c.debug {
279 slog.Debug("Waiting for LSP server to be ready...")
283 slog.Debug("Waiting for LSP server to be ready...")
280 }
284 }
281
285
@@ -289,7 +293,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
289 case <-ticker.C:
293 case <-ticker.C:
290 // Check if client is running
294 // Check if client is running
291 if !c.client.IsRunning() {
295 if !c.client.IsRunning() {
292− if cfg != nil && cfg.Options.DebugLSP {
296+ if c.debug {
293 slog.Debug("LSP server not ready yet", "server", c.name)
297 slog.Debug("LSP server not ready yet", "server", c.name)
294 }
298 }
295 continue
299 continue
@@ -297,7 +301,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
297
301
298 // Server is ready
302 // Server is ready
299 c.SetServerState(StateReady)
303 c.SetServerState(StateReady)
300− if cfg != nil && cfg.Options.DebugLSP {
304+ if c.debug {
301 slog.Debug("LSP server is ready")
305 slog.Debug("LSP server is ready")
302 }
306 }
303 return nil
307 return nil
@@ -416,10 +420,8 @@ func (c *Client) IsFileOpen(filepath string) bool {
416
420
417// CloseAllFiles closes all currently open files.
421// CloseAllFiles closes all currently open files.
418func (c *Client) CloseAllFiles(ctx context.Context) {
422func (c *Client) CloseAllFiles(ctx context.Context) {
419− cfg := config.Get()
420− debugLSP := cfg != nil && cfg.Options.DebugLSP
421 for uri := range c.openFiles.Seq2() {
423 for uri := range c.openFiles.Seq2() {
422− if debugLSP {
424+ if c.debug {
423 slog.Debug("Closing file", "file", uri)
425 slog.Debug("Closing file", "file", uri)
424 }
426 }
425 if err := c.client.NotifyDidCloseTextDocument(ctx, uri); err != nil {
427 if err := c.client.NotifyDidCloseTextDocument(ctx, uri); err != nil {
internal/lsp/client_test.go+1−1
@@ -23,7 +23,7 @@ func TestClient(t *testing.T) {
23 // but we can still test the basic structure
23 // but we can still test the basic structure
24 client, err := New(ctx, "test", cfg, config.NewEnvironmentVariableResolver(env.NewFromMap(map[string]string{
24 client, err := New(ctx, "test", cfg, config.NewEnvironmentVariableResolver(env.NewFromMap(map[string]string{
25 "THE_CMD": "echo",
25 "THE_CMD": "echo",
26− })))
26+ })), false)
27 if err != nil {
27 if err != nil {
28 // Expected to fail with echo command, skip the rest
28 // Expected to fail with echo command, skip the rest
29 t.Skipf("Powernap client creation failed as expected with dummy command: %v", err)
29 t.Skipf("Powernap client creation failed as expected with dummy command: %v", err)
internal/lsp/handlers.go−6
@@ -5,7 +5,6 @@ import (
5 "encoding/json"
5 "encoding/json"
6 "log/slog"
6 "log/slog"
7
7
8− "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/lsp/util"
8 "github.com/charmbracelet/crush/internal/lsp/util"
10 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
9 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
11)
10)
@@ -80,11 +79,6 @@ func notifyFileWatchRegistration(id string, watchers []protocol.FileSystemWatche
80
79
81// HandleServerMessage handles server messages
80// HandleServerMessage handles server messages
82func HandleServerMessage(_ context.Context, method string, params json.RawMessage) {
81func HandleServerMessage(_ context.Context, method string, params json.RawMessage) {
83− cfg := config.Get()
84− if !cfg.Options.DebugLSP {
85− return
86− }
87−
88 var msg protocol.ShowMessageParams
82 var msg protocol.ShowMessageParams
89 if err := json.Unmarshal(params, &msg); err != nil {
83 if err := json.Unmarshal(params, &msg); err != nil {
90 slog.Debug("Server message", "type", msg.Type, "message", msg.Message)
84 slog.Debug("Server message", "type", msg.Type, "message", msg.Message)
internal/ui/chat/messages.go+5−3
@@ -186,16 +186,18 @@ type AssistantInfoItem struct {
186 id string
186 id string
187 message *message.Message
187 message *message.Message
188 sty *styles.Styles
188 sty *styles.Styles
189+ cfg *config.Config
189 lastUserMessageTime time.Time
190 lastUserMessageTime time.Time
190}
191}
191
192
192// NewAssistantInfoItem creates a new AssistantInfoItem.
193// NewAssistantInfoItem creates a new AssistantInfoItem.
193−func NewAssistantInfoItem(sty *styles.Styles, message *message.Message, lastUserMessageTime time.Time) MessageItem {
194+func NewAssistantInfoItem(sty *styles.Styles, message *message.Message, cfg *config.Config, lastUserMessageTime time.Time) MessageItem {
194 return &AssistantInfoItem{
195 return &AssistantInfoItem{
195 cachedMessageItem: &cachedMessageItem{},
196 cachedMessageItem: &cachedMessageItem{},
196 id: AssistantInfoID(message.ID),
197 id: AssistantInfoID(message.ID),
197 message: message,
198 message: message,
198 sty: sty,
199 sty: sty,
200+ cfg: cfg,
199 lastUserMessageTime: lastUserMessageTime,
201 lastUserMessageTime: lastUserMessageTime,
200 }
202 }
201}
203}
@@ -231,13 +233,13 @@ func (a *AssistantInfoItem) renderContent(width int) string {
231 duration := finishTime.Sub(a.lastUserMessageTime)
233 duration := finishTime.Sub(a.lastUserMessageTime)
232 infoMsg := a.sty.Chat.Message.AssistantInfoDuration.Render(duration.String())
234 infoMsg := a.sty.Chat.Message.AssistantInfoDuration.Render(duration.String())
233 icon := a.sty.Chat.Message.AssistantInfoIcon.Render(styles.ModelIcon)
235 icon := a.sty.Chat.Message.AssistantInfoIcon.Render(styles.ModelIcon)
234− model := config.Get().GetModel(a.message.Provider, a.message.Model)
236+ model := a.cfg.GetModel(a.message.Provider, a.message.Model)
235 if model == nil {
237 if model == nil {
236 model = &catwalk.Model{Name: "Unknown Model"}
238 model = &catwalk.Model{Name: "Unknown Model"}
237 }
239 }
238 modelFormatted := a.sty.Chat.Message.AssistantInfoModel.Render(model.Name)
240 modelFormatted := a.sty.Chat.Message.AssistantInfoModel.Render(model.Name)
239 providerName := a.message.Provider
241 providerName := a.message.Provider
240− if providerConfig, ok := config.Get().Providers.Get(a.message.Provider); ok {
242+ if providerConfig, ok := a.cfg.Providers.Get(a.message.Provider); ok {
241 providerName = providerConfig.Name
243 providerName = providerConfig.Name
242 }
244 }
243 provider := a.sty.Chat.Message.AssistantInfoProvider.Render(fmt.Sprintf("via %s", providerName))
245 provider := a.sty.Chat.Message.AssistantInfoProvider.Render(fmt.Sprintf("via %s", providerName))
internal/ui/dialog/api_key_input.go+1−1
@@ -296,7 +296,7 @@ func (m *APIKeyInput) verifyAPIKey() tea.Msg {
296 Type: m.provider.Type,
296 Type: m.provider.Type,
297 BaseURL: m.provider.APIEndpoint,
297 BaseURL: m.provider.APIEndpoint,
298 }
298 }
299− err := providerConfig.TestConnection(config.Get().Resolver())
299+ err := providerConfig.TestConnection(m.com.Config().Resolver())
300
300
301 // intentionally wait for at least 750ms to make sure the user sees the spinner
301 // intentionally wait for at least 750ms to make sure the user sees the spinner
302 elapsed := time.Since(start)
302 elapsed := time.Since(start)
internal/ui/model/header.go+2−2
@@ -117,8 +117,8 @@ func renderHeaderDetails(
117 parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPErrorIcon, errorCount)))
117 parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPErrorIcon, errorCount)))
118 }
118 }
119
119
120− agentCfg := config.Get().Agents[config.AgentCoder]
120+ agentCfg := com.Config().Agents[config.AgentCoder]
121− model := config.Get().GetModelByType(agentCfg.Model)
121+ model := com.Config().GetModelByType(agentCfg.Model)
122 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
122 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
123 formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
123 formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
124 parts = append(parts, formattedPercentage)
124 parts = append(parts, formattedPercentage)
internal/ui/model/onboarding.go+1−1
@@ -19,7 +19,7 @@ import (
19// markProjectInitialized marks the current project as initialized in the config.
19// markProjectInitialized marks the current project as initialized in the config.
20func (m *UI) markProjectInitialized() tea.Msg {
20func (m *UI) markProjectInitialized() tea.Msg {
21 // TODO: handle error so we show it in the tui footer
21 // TODO: handle error so we show it in the tui footer
22− err := config.MarkProjectInitialized()
22+ err := config.MarkProjectInitialized(m.com.Config())
23 if err != nil {
23 if err != nil {
24 slog.Error(err.Error())
24 slog.Error(err.Error())
25 }
25 }
internal/ui/model/ui.go+6−6
@@ -298,7 +298,7 @@ func New(com *common.Common) *UI {
298 desiredFocus := uiFocusEditor
298 desiredFocus := uiFocusEditor
299 if !com.Config().IsConfigured() {
299 if !com.Config().IsConfigured() {
300 desiredState = uiOnboarding
300 desiredState = uiOnboarding
301− } else if n, _ := config.ProjectNeedsInitialization(); n {
301+ } else if n, _ := config.ProjectNeedsInitialization(com.Config()); n {
302 desiredState = uiInitialize
302 desiredState = uiInitialize
303 }
303 }
304
304
@@ -776,7 +776,7 @@ func (m *UI) setSessionMessages(msgs []message.Message) tea.Cmd {
776 case message.Assistant:
776 case message.Assistant:
777 items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap)...)
777 items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap)...)
778 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
778 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
779− infoItem := chat.NewAssistantInfoItem(m.com.Styles, msg, time.Unix(m.lastUserMessageTime, 0))
779+ infoItem := chat.NewAssistantInfoItem(m.com.Styles, msg, m.com.Config(), time.Unix(m.lastUserMessageTime, 0))
780 items = append(items, infoItem)
780 items = append(items, infoItem)
781 }
781 }
782 default:
782 default:
@@ -906,7 +906,7 @@ func (m *UI) appendSessionMessage(msg message.Message) tea.Cmd {
906 }
906 }
907 }
907 }
908 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
908 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
909− infoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, time.Unix(m.lastUserMessageTime, 0))
909+ infoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, m.com.Config(), time.Unix(m.lastUserMessageTime, 0))
910 m.chat.AppendMessages(infoItem)
910 m.chat.AppendMessages(infoItem)
911 if atBottom {
911 if atBottom {
912 if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
912 if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
@@ -977,7 +977,7 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
977
977
978 if shouldRenderAssistant && msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
978 if shouldRenderAssistant && msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
979 if infoItem := m.chat.MessageItem(chat.AssistantInfoID(msg.ID)); infoItem == nil {
979 if infoItem := m.chat.MessageItem(chat.AssistantInfoID(msg.ID)); infoItem == nil {
980− newInfoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, time.Unix(m.lastUserMessageTime, 0))
980+ newInfoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, m.com.Config(), time.Unix(m.lastUserMessageTime, 0))
981 m.chat.AppendMessages(newInfoItem)
981 m.chat.AppendMessages(newInfoItem)
982 }
982 }
983 }
983 }
@@ -1249,7 +1249,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1249
1249
1250 // Attempt to import GitHub Copilot tokens from VSCode if available.
1250 // Attempt to import GitHub Copilot tokens from VSCode if available.
1251 if isCopilot && !isConfigured() {
1251 if isCopilot && !isConfigured() {
1252− config.Get().ImportCopilot()
1252+ m.com.Config().ImportCopilot()
1253 }
1253 }
1254
1254
1255 if !isConfigured() {
1255 if !isConfigured() {
@@ -3063,7 +3063,7 @@ func (m *UI) drawSessionDetails(scr uv.Screen, area uv.Rectangle) {
3063
3063
3064func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string) tea.Cmd {
3064func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string) tea.Cmd {
3065 load := func() tea.Msg {
3065 load := func() tea.Msg {
3066− prompt, err := commands.GetMCPPrompt(clientID, promptID, arguments)
3066+ prompt, err := commands.GetMCPPrompt(m.com.Config(), clientID, promptID, arguments)
3067 if err != nil {
3067 if err != nil {
3068 // TODO: make this better
3068 // TODO: make this better
3069 return util.ReportError(err)()
3069 return util.ReportError(err)()