Detailed changes
  
  
    
    @@ -35,6 +35,9 @@ type App struct {
 
 	LSPClients *csync.Map[string, *lsp.Client]
 
+	lspStates *csync.Map[string, LSPClientInfo]
+	lspBroker *pubsub.Broker[LSPEvent]
+
 	config *config.Config
 
 	serviceEventsWG *sync.WaitGroup
@@ -65,6 +68,8 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
 		History:     files,
 		Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools),
 		LSPClients:  csync.NewMap[string, *lsp.Client](),
+		lspStates:   csync.NewMap[string, LSPClientInfo](),
+		lspBroker:   pubsub.NewBroker[LSPEvent](),
 
 		globalCtx: ctx,
 
@@ -220,7 +225,7 @@ func (app *App) setupEvents() {
 	setupSubscriber(ctx, app.serviceEventsWG, "permissions-notifications", app.Permissions.SubscribeNotifications, app.events)
 	setupSubscriber(ctx, app.serviceEventsWG, "history", app.History.Subscribe, app.events)
 	setupSubscriber(ctx, app.serviceEventsWG, "mcp", agent.SubscribeMCPEvents, app.events)
-	setupSubscriber(ctx, app.serviceEventsWG, "lsp", SubscribeLSPEvents, app.events)
+	setupSubscriber(ctx, app.serviceEventsWG, "lsp", app.SubscribeLSPEvents, app.events)
 	cleanupFunc := func() error {
 		cancel()
 		app.serviceEventsWG.Wait()
  
  
  
    
    @@ -28,23 +28,23 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
 	// Check if any root markers exist in the working directory (config now has defaults)
 	if !lsp.HasRootMarkers(app.config.WorkingDir(), config.RootMarkers) {
 		slog.Info("Skipping LSP client - no root markers found", "name", name, "rootMarkers", config.RootMarkers)
-		updateLSPState(name, lsp.StateDisabled, nil, nil, 0)
+		app.updateLSPState(name, lsp.StateDisabled, nil, 0)
 		return
 	}
 
 	// Update state to starting
-	updateLSPState(name, lsp.StateStarting, nil, nil, 0)
+	app.updateLSPState(name, lsp.StateStarting, nil, 0)
 
 	// Create LSP client.
 	lspClient, err := lsp.New(ctx, name, config)
 	if err != nil {
 		slog.Error("Failed to create LSP client for", name, err)
-		updateLSPState(name, lsp.StateError, err, nil, 0)
+		app.updateLSPState(name, lsp.StateError, err, 0)
 		return
 	}
 
 	// Set diagnostics callback
-	lspClient.SetDiagnosticsCallback(updateLSPDiagnostics)
+	lspClient.SetDiagnosticsCallback(app.updateLSPDiagnostics)
 
 	// Increase initialization timeout as some servers take more time to start.
 	initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
@@ -54,7 +54,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
 	_, err = lspClient.Initialize(initCtx, app.config.WorkingDir())
 	if err != nil {
 		slog.Error("Initialize failed", "name", name, "error", err)
-		updateLSPState(name, lsp.StateError, err, lspClient, 0)
+		app.updateLSPState(name, lsp.StateError, err, 0)
 		lspClient.Close(ctx)
 		return
 	}
@@ -65,12 +65,12 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
 		// Server never reached a ready state, but let's continue anyway, as
 		// some functionality might still work.
 		lspClient.SetServerState(lsp.StateError)
-		updateLSPState(name, lsp.StateError, err, lspClient, 0)
+		app.updateLSPState(name, lsp.StateError, err, 0)
 	} else {
 		// Server reached a ready state scuccessfully.
 		slog.Info("LSP server is ready", "name", name)
 		lspClient.SetServerState(lsp.StateReady)
-		updateLSPState(name, lsp.StateReady, nil, lspClient, 0)
+		app.updateLSPState(name, lsp.StateReady, nil, 0)
 	}
 
 	slog.Info("LSP client initialized", "name", name)
  
  
  
    
    @@ -5,7 +5,6 @@ import (
 	"maps"
 	"time"
 
-	"github.com/charmbracelet/crush/internal/csync"
 	"github.com/charmbracelet/crush/internal/lsp"
 	"github.com/charmbracelet/crush/internal/pubsub"
 )
@@ -38,50 +37,43 @@ type LSPEvent struct {
 
 // LSPClientInfo holds information about an LSP client's state
 type LSPClientInfo struct {
-	Name            string
-	State           lsp.ServerState
-	Error           error
-	Client          *lsp.Client
-	DiagnosticCount int
-	ConnectedAt     time.Time
+	Name            string          `json:"name"`
+	State           lsp.ServerState `json:"state"`
+	Error           error           `json:"error,omitempty"`
+	DiagnosticCount int             `json:"diagnostic_count,omitempty"`
+	ConnectedAt     time.Time       `json:"connected_at"`
 }
 
-var (
-	lspStates = csync.NewMap[string, LSPClientInfo]()
-	lspBroker = pubsub.NewBroker[LSPEvent]()
-)
-
 // SubscribeLSPEvents returns a channel for LSP events
-func SubscribeLSPEvents(ctx context.Context) <-chan pubsub.Event[LSPEvent] {
-	return lspBroker.Subscribe(ctx)
+func (a *App) SubscribeLSPEvents(ctx context.Context) <-chan pubsub.Event[LSPEvent] {
+	return a.lspBroker.Subscribe(ctx)
 }
 
 // GetLSPStates returns the current state of all LSP clients
-func GetLSPStates() map[string]LSPClientInfo {
-	return maps.Collect(lspStates.Seq2())
+func (a *App) GetLSPStates() map[string]LSPClientInfo {
+	return maps.Collect(a.lspStates.Seq2())
 }
 
 // GetLSPState returns the state of a specific LSP client
-func GetLSPState(name string) (LSPClientInfo, bool) {
-	return lspStates.Get(name)
+func (a *App) GetLSPState(name string) (LSPClientInfo, bool) {
+	return a.lspStates.Get(name)
 }
 
 // updateLSPState updates the state of an LSP client and publishes an event
-func updateLSPState(name string, state lsp.ServerState, err error, client *lsp.Client, diagnosticCount int) {
+func (a *App) updateLSPState(name string, state lsp.ServerState, err error, diagnosticCount int) {
 	info := LSPClientInfo{
 		Name:            name,
 		State:           state,
 		Error:           err,
-		Client:          client,
 		DiagnosticCount: diagnosticCount,
 	}
 	if state == lsp.StateReady {
 		info.ConnectedAt = time.Now()
 	}
-	lspStates.Set(name, info)
+	a.lspStates.Set(name, info)
 
 	// Publish state change event
-	lspBroker.Publish(pubsub.UpdatedEvent, LSPEvent{
+	a.lspBroker.Publish(pubsub.UpdatedEvent, LSPEvent{
 		Type:            LSPEventStateChanged,
 		Name:            name,
 		State:           state,
@@ -91,13 +83,13 @@ func updateLSPState(name string, state lsp.ServerState, err error, client *lsp.C
 }
 
 // updateLSPDiagnostics updates the diagnostic count for an LSP client and publishes an event
-func updateLSPDiagnostics(name string, diagnosticCount int) {
-	if info, exists := lspStates.Get(name); exists {
+func (a *App) updateLSPDiagnostics(name string, diagnosticCount int) {
+	if info, exists := a.lspStates.Get(name); exists {
 		info.DiagnosticCount = diagnosticCount
-		lspStates.Set(name, info)
+		a.lspStates.Set(name, info)
 
 		// Publish diagnostics change event
-		lspBroker.Publish(pubsub.UpdatedEvent, LSPEvent{
+		a.lspBroker.Publish(pubsub.UpdatedEvent, LSPEvent{
 			Type:            LSPEventDiagnosticsChanged,
 			Name:            name,
 			State:           info.State,
  
  
  
    
    @@ -1,8 +1,47 @@
 package message
 
+import (
+	"encoding/base64"
+	"encoding/json"
+)
+
 type Attachment struct {
-	FilePath string
-	FileName string
-	MimeType string
-	Content  []byte
+	FilePath string `json:"file_path"`
+	FileName string `json:"file_name"`
+	MimeType string `json:"mime_type"`
+	Content  []byte `json:"content"`
+}
+
+// MarshalJSON implements the [json.Marshaler] interface.
+func (a Attachment) MarshalJSON() ([]byte, error) {
+	// Encode the content as a base64 string
+	type Alias Attachment
+	return json.Marshal(&struct {
+		Content string `json:"content"`
+		*Alias
+	}{
+		Content: base64.StdEncoding.EncodeToString(a.Content),
+		Alias:   (*Alias)(&a),
+	})
+}
+
+// UnmarshalJSON implements the [json.Unmarshaler] interface.
+func (a *Attachment) UnmarshalJSON(data []byte) error {
+	// Decode the content from a base64 string
+	type Alias Attachment
+	aux := &struct {
+		Content string `json:"content"`
+		*Alias
+	}{
+		Alias: (*Alias)(a),
+	}
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+	content, err := base64.StdEncoding.DecodeString(aux.Content)
+	if err != nil {
+		return err
+	}
+	a.Content = content
+	return nil
 }