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 case pubsub.Event[proto.ConfigChanged]:
95 return envelope(pubsub.PayloadTypeConfigChanged, e)
96 default:
97 slog.Warn("Unrecognized event type for SSE wrapping", "type", fmt.Sprintf("%T", ev))
98 return nil
99 }
100}
101
102// envelope marshals the inner event and wraps it in a pubsub.Payload.
103func envelope(payloadType pubsub.PayloadType, inner any) *pubsub.Payload {
104 raw, err := json.Marshal(inner)
105 if err != nil {
106 slog.Error("Failed to marshal event payload", "error", err)
107 return nil
108 }
109 return &pubsub.Payload{
110 Type: payloadType,
111 Payload: raw,
112 }
113}
114
115func mcpEventTypeToProto(t mcp.EventType) proto.MCPEventType {
116 switch t {
117 case mcp.EventStateChanged:
118 return proto.MCPEventStateChanged
119 case mcp.EventToolsListChanged:
120 return proto.MCPEventToolsListChanged
121 case mcp.EventPromptsListChanged:
122 return proto.MCPEventPromptsListChanged
123 case mcp.EventResourcesListChanged:
124 return proto.MCPEventResourcesListChanged
125 default:
126 return proto.MCPEventStateChanged
127 }
128}
129
130func sessionToProto(s session.Session) proto.Session {
131 return proto.Session{
132 ID: s.ID,
133 ParentSessionID: s.ParentSessionID,
134 Title: s.Title,
135 SummaryMessageID: s.SummaryMessageID,
136 MessageCount: s.MessageCount,
137 PromptTokens: s.PromptTokens,
138 CompletionTokens: s.CompletionTokens,
139 Cost: s.Cost,
140 Todos: todosToProto(s.Todos),
141 CreatedAt: s.CreatedAt,
142 UpdatedAt: s.UpdatedAt,
143 }
144}
145
146func todosToProto(todos []session.Todo) []proto.Todo {
147 if len(todos) == 0 {
148 return nil
149 }
150 out := make([]proto.Todo, len(todos))
151 for i, t := range todos {
152 out[i] = proto.Todo{
153 Content: t.Content,
154 Status: string(t.Status),
155 ActiveForm: t.ActiveForm,
156 }
157 }
158 return out
159}
160
161func fileToProto(f history.File) proto.File {
162 return proto.File{
163 ID: f.ID,
164 SessionID: f.SessionID,
165 Path: f.Path,
166 Content: f.Content,
167 Version: f.Version,
168 CreatedAt: f.CreatedAt,
169 UpdatedAt: f.UpdatedAt,
170 }
171}
172
173func messageToProto(m message.Message) proto.Message {
174 msg := proto.Message{
175 ID: m.ID,
176 SessionID: m.SessionID,
177 Role: proto.MessageRole(m.Role),
178 Model: m.Model,
179 Provider: m.Provider,
180 CreatedAt: m.CreatedAt,
181 UpdatedAt: m.UpdatedAt,
182 }
183
184 for _, p := range m.Parts {
185 switch v := p.(type) {
186 case message.TextContent:
187 msg.Parts = append(msg.Parts, proto.TextContent{Text: v.Text})
188 case message.ReasoningContent:
189 msg.Parts = append(msg.Parts, proto.ReasoningContent{
190 Thinking: v.Thinking,
191 Signature: v.Signature,
192 StartedAt: v.StartedAt,
193 FinishedAt: v.FinishedAt,
194 })
195 case message.ToolCall:
196 msg.Parts = append(msg.Parts, proto.ToolCall{
197 ID: v.ID,
198 Name: v.Name,
199 Input: v.Input,
200 Finished: v.Finished,
201 })
202 case message.ToolResult:
203 msg.Parts = append(msg.Parts, proto.ToolResult{
204 ToolCallID: v.ToolCallID,
205 Name: v.Name,
206 Content: v.Content,
207 Data: v.Data,
208 MIMEType: v.MIMEType,
209 Metadata: v.Metadata,
210 IsError: v.IsError,
211 })
212 case message.Finish:
213 msg.Parts = append(msg.Parts, proto.Finish{
214 Reason: proto.FinishReason(v.Reason),
215 Time: v.Time,
216 Message: v.Message,
217 Details: v.Details,
218 })
219 case message.ImageURLContent:
220 msg.Parts = append(msg.Parts, proto.ImageURLContent{URL: v.URL, Detail: v.Detail})
221 case message.BinaryContent:
222 msg.Parts = append(msg.Parts, proto.BinaryContent{Path: v.Path, MIMEType: v.MIMEType, Data: v.Data})
223 }
224 }
225
226 return msg
227}
228
229func messagesToProto(msgs []message.Message) []proto.Message {
230 out := make([]proto.Message, len(msgs))
231 for i, m := range msgs {
232 out[i] = messageToProto(m)
233 }
234 return out
235}