events.go

  1package server
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"log/slog"
  7
  8	"github.com/charmbracelet/crush/internal/agent/notify"
  9	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 10	"github.com/charmbracelet/crush/internal/app"
 11	"github.com/charmbracelet/crush/internal/history"
 12	"github.com/charmbracelet/crush/internal/message"
 13	"github.com/charmbracelet/crush/internal/permission"
 14	"github.com/charmbracelet/crush/internal/proto"
 15	"github.com/charmbracelet/crush/internal/pubsub"
 16	"github.com/charmbracelet/crush/internal/session"
 17)
 18
 19// wrapEvent converts a raw tea.Msg (a pubsub.Event[T] from the app
 20// event fan-in) into a pubsub.Payload envelope with the correct
 21// PayloadType discriminator and a proto-typed inner payload that has
 22// proper JSON tags. Returns nil if the event type is unrecognized.
 23func wrapEvent(ev any) *pubsub.Payload {
 24	switch e := ev.(type) {
 25	case pubsub.Event[app.LSPEvent]:
 26		return envelope(pubsub.PayloadTypeLSPEvent, pubsub.Event[proto.LSPEvent]{
 27			Type: e.Type,
 28			Payload: proto.LSPEvent{
 29				Type:            proto.LSPEventType(e.Payload.Type),
 30				Name:            e.Payload.Name,
 31				State:           e.Payload.State,
 32				Error:           e.Payload.Error,
 33				DiagnosticCount: e.Payload.DiagnosticCount,
 34			},
 35		})
 36	case pubsub.Event[mcp.Event]:
 37		return envelope(pubsub.PayloadTypeMCPEvent, pubsub.Event[proto.MCPEvent]{
 38			Type: e.Type,
 39			Payload: proto.MCPEvent{
 40				Type:      mcpEventTypeToProto(e.Payload.Type),
 41				Name:      e.Payload.Name,
 42				State:     proto.MCPState(e.Payload.State),
 43				Error:     e.Payload.Error,
 44				ToolCount: e.Payload.Counts.Tools,
 45			},
 46		})
 47	case pubsub.Event[permission.PermissionRequest]:
 48		return envelope(pubsub.PayloadTypePermissionRequest, pubsub.Event[proto.PermissionRequest]{
 49			Type: e.Type,
 50			Payload: proto.PermissionRequest{
 51				ID:          e.Payload.ID,
 52				SessionID:   e.Payload.SessionID,
 53				ToolCallID:  e.Payload.ToolCallID,
 54				ToolName:    e.Payload.ToolName,
 55				Description: e.Payload.Description,
 56				Action:      e.Payload.Action,
 57				Path:        e.Payload.Path,
 58				Params:      e.Payload.Params,
 59			},
 60		})
 61	case pubsub.Event[permission.PermissionNotification]:
 62		return envelope(pubsub.PayloadTypePermissionNotification, pubsub.Event[proto.PermissionNotification]{
 63			Type: e.Type,
 64			Payload: proto.PermissionNotification{
 65				ToolCallID: e.Payload.ToolCallID,
 66				Granted:    e.Payload.Granted,
 67				Denied:     e.Payload.Denied,
 68			},
 69		})
 70	case pubsub.Event[message.Message]:
 71		return envelope(pubsub.PayloadTypeMessage, pubsub.Event[proto.Message]{
 72			Type:    e.Type,
 73			Payload: messageToProto(e.Payload),
 74		})
 75	case pubsub.Event[session.Session]:
 76		return envelope(pubsub.PayloadTypeSession, pubsub.Event[proto.Session]{
 77			Type:    e.Type,
 78			Payload: sessionToProto(e.Payload),
 79		})
 80	case pubsub.Event[history.File]:
 81		return envelope(pubsub.PayloadTypeFile, pubsub.Event[proto.File]{
 82			Type:    e.Type,
 83			Payload: fileToProto(e.Payload),
 84		})
 85	case pubsub.Event[notify.Notification]:
 86		return envelope(pubsub.PayloadTypeAgentEvent, pubsub.Event[proto.AgentEvent]{
 87			Type: e.Type,
 88			Payload: proto.AgentEvent{
 89				SessionID:    e.Payload.SessionID,
 90				SessionTitle: e.Payload.SessionTitle,
 91				Type:         proto.AgentEventType(e.Payload.Type),
 92			},
 93		})
 94	default:
 95		slog.Warn("Unrecognized event type for SSE wrapping", "type", fmt.Sprintf("%T", ev))
 96		return nil
 97	}
 98}
 99
100// envelope marshals the inner event and wraps it in a pubsub.Payload.
101func envelope(payloadType pubsub.PayloadType, inner any) *pubsub.Payload {
102	raw, err := json.Marshal(inner)
103	if err != nil {
104		slog.Error("Failed to marshal event payload", "error", err)
105		return nil
106	}
107	return &pubsub.Payload{
108		Type:    payloadType,
109		Payload: raw,
110	}
111}
112
113func mcpEventTypeToProto(t mcp.EventType) proto.MCPEventType {
114	switch t {
115	case mcp.EventStateChanged:
116		return proto.MCPEventStateChanged
117	case mcp.EventToolsListChanged:
118		return proto.MCPEventToolsListChanged
119	case mcp.EventPromptsListChanged:
120		return proto.MCPEventPromptsListChanged
121	case mcp.EventResourcesListChanged:
122		return proto.MCPEventResourcesListChanged
123	default:
124		return proto.MCPEventStateChanged
125	}
126}
127
128func sessionToProto(s session.Session) proto.Session {
129	return proto.Session{
130		ID:               s.ID,
131		ParentSessionID:  s.ParentSessionID,
132		Title:            s.Title,
133		SummaryMessageID: s.SummaryMessageID,
134		MessageCount:     s.MessageCount,
135		PromptTokens:     s.PromptTokens,
136		CompletionTokens: s.CompletionTokens,
137		Cost:             s.Cost,
138		CreatedAt:        s.CreatedAt,
139		UpdatedAt:        s.UpdatedAt,
140	}
141}
142
143func fileToProto(f history.File) proto.File {
144	return proto.File{
145		ID:        f.ID,
146		SessionID: f.SessionID,
147		Path:      f.Path,
148		Content:   f.Content,
149		Version:   f.Version,
150		CreatedAt: f.CreatedAt,
151		UpdatedAt: f.UpdatedAt,
152	}
153}
154
155func messageToProto(m message.Message) proto.Message {
156	msg := proto.Message{
157		ID:        m.ID,
158		SessionID: m.SessionID,
159		Role:      proto.MessageRole(m.Role),
160		Model:     m.Model,
161		Provider:  m.Provider,
162		CreatedAt: m.CreatedAt,
163		UpdatedAt: m.UpdatedAt,
164	}
165
166	for _, p := range m.Parts {
167		switch v := p.(type) {
168		case message.TextContent:
169			msg.Parts = append(msg.Parts, proto.TextContent{Text: v.Text})
170		case message.ReasoningContent:
171			msg.Parts = append(msg.Parts, proto.ReasoningContent{
172				Thinking:   v.Thinking,
173				Signature:  v.Signature,
174				StartedAt:  v.StartedAt,
175				FinishedAt: v.FinishedAt,
176			})
177		case message.ToolCall:
178			msg.Parts = append(msg.Parts, proto.ToolCall{
179				ID:       v.ID,
180				Name:     v.Name,
181				Input:    v.Input,
182				Finished: v.Finished,
183			})
184		case message.ToolResult:
185			msg.Parts = append(msg.Parts, proto.ToolResult{
186				ToolCallID: v.ToolCallID,
187				Name:       v.Name,
188				Content:    v.Content,
189				IsError:    v.IsError,
190			})
191		case message.Finish:
192			msg.Parts = append(msg.Parts, proto.Finish{
193				Reason:  proto.FinishReason(v.Reason),
194				Time:    v.Time,
195				Message: v.Message,
196				Details: v.Details,
197			})
198		case message.ImageURLContent:
199			msg.Parts = append(msg.Parts, proto.ImageURLContent{URL: v.URL, Detail: v.Detail})
200		case message.BinaryContent:
201			msg.Parts = append(msg.Parts, proto.BinaryContent{Path: v.Path, MIMEType: v.MIMEType, Data: v.Data})
202		}
203	}
204
205	return msg
206}
207
208func messagesToProto(msgs []message.Message) []proto.Message {
209	out := make([]proto.Message, len(msgs))
210	for i, m := range msgs {
211		out[i] = messageToProto(m)
212	}
213	return out
214}