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