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