diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index fbb5b4fd8c6390ff0dfad0e072af35342355ba41..3d2012f04812c234bd9f50cb7c2246c977c190de 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -403,7 +403,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory) if err != nil { if errors.Is(err, context.Canceled) { - agentMessage.AddFinish(message.FinishReasonCanceled) + agentMessage.AddFinish(message.FinishReasonCanceled, "Request cancelled", "") a.messages.Update(context.Background(), agentMessage) return a.err(ErrRequestCancelled) } @@ -454,11 +454,15 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg // Process each event in the stream. for event := range eventChan { if processErr := a.processEvent(ctx, sessionID, &assistantMsg, event); processErr != nil { - a.finishMessage(ctx, &assistantMsg, message.FinishReasonCanceled) + if errors.Is(processErr, context.Canceled) { + a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "") + } else { + a.finishMessage(ctx, &assistantMsg, message.FinishReasonError, "API Error", processErr.Error()) + } return assistantMsg, nil, processErr } if ctx.Err() != nil { - a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled) + a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "") return assistantMsg, nil, ctx.Err() } } @@ -468,7 +472,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg for i, toolCall := range toolCalls { select { case <-ctx.Done(): - a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled) + a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "") // Make all future tool calls cancelled for j := i; j < len(toolCalls); j++ { toolResults[j] = message.ToolResult{ @@ -516,7 +520,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg IsError: true, } } - a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied) + a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied, "Permission denied", "") break } } @@ -548,8 +552,8 @@ out: return assistantMsg, &msg, err } -func (a *agent) finishMessage(ctx context.Context, msg *message.Message, finishReson message.FinishReason) { - msg.AddFinish(finishReson) +func (a *agent) finishMessage(ctx context.Context, msg *message.Message, finishReson message.FinishReason, message, details string) { + msg.AddFinish(finishReson, message, details) _ = a.messages.Update(ctx, *msg) } @@ -580,15 +584,10 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg assistantMsg.FinishToolCall(event.ToolCall.ID) return a.messages.Update(ctx, *assistantMsg) case provider.EventError: - if errors.Is(event.Error, context.Canceled) { - slog.Info(fmt.Sprintf("Event processing canceled for session: %s", sessionID)) - return context.Canceled - } - slog.Error(event.Error.Error()) return event.Error case provider.EventComplete: assistantMsg.SetToolCalls(event.Response.ToolCalls) - assistantMsg.AddFinish(event.Response.FinishReason) + assistantMsg.AddFinish(event.Response.FinishReason, "", "") if err := a.messages.Update(ctx, *assistantMsg); err != nil { return fmt.Errorf("failed to update message: %w", err) } diff --git a/internal/message/content.go b/internal/message/content.go index 3ab53e381aaf7755c141985ebe740dbc44356471..b8d2c1aa370559977f4c8eb80803ab5fbfe83cf9 100644 --- a/internal/message/content.go +++ b/internal/message/content.go @@ -102,8 +102,10 @@ type ToolResult struct { func (ToolResult) isPart() {} type Finish struct { - Reason FinishReason `json:"reason"` - Time int64 `json:"time"` + Reason FinishReason `json:"reason"` + Time int64 `json:"time"` + Message string `json:"message,omitempty"` + Details string `json:"details,omitempty"` } func (Finish) isPart() {} @@ -308,7 +310,7 @@ func (m *Message) SetToolResults(tr []ToolResult) { } } -func (m *Message) AddFinish(reason FinishReason) { +func (m *Message) AddFinish(reason FinishReason, message, details string) { // remove any existing finish part for i, part := range m.Parts { if _, ok := part.(Finish); ok { @@ -316,7 +318,7 @@ func (m *Message) AddFinish(reason FinishReason) { break } } - m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix()}) + m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix(), Message: message, Details: details}) } func (m *Message) AddImageURL(url, detail string) { diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index e8ae97056728a0377ddcad179ecae1246f2da662..657533d9f0f13fcad404f76d8e19999710800050 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -8,6 +8,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/fur/provider" @@ -184,6 +185,7 @@ func (m *messageCmp) toMarkdown(content string) string { // markdownContent processes the message content and handles special states. // Returns appropriate content for thinking, finished, and error states. func (m *messageCmp) markdownContent() string { + t := styles.CurrentTheme() content := m.message.Content().String() if m.message.Role == message.Assistant { thinking := m.message.IsThinking() @@ -199,6 +201,13 @@ func (m *messageCmp) markdownContent() string { content = "" } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled { content = "*Canceled*" + } else if finished && content == "" && finishedData.Reason == message.FinishReasonError { + errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR") + truncated := ansi.Truncate(finishedData.Message, m.textWidth()-2-lipgloss.Width(errTag), "...") + title := fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(truncated)) + details := t.S().Base.Foreground(t.FgSubtle).Width(m.textWidth() - 2).Render(finishedData.Details) + return fmt.Sprintf("%s\n\n%s", title, details) + } } return m.toMarkdown(content)