fix: improve ux for presenting errors from providers (#1388)

Andrey Nering created

Change summary

go.mod                       |  2 +-
go.sum                       |  4 ++--
internal/agent/agent.go      | 13 +++++++++++--
internal/stringext/string.go | 10 ++++++++++
4 files changed, 24 insertions(+), 5 deletions(-)

Detailed changes

go.mod 🔗

@@ -5,7 +5,7 @@ go 1.25.0
 require (
 	charm.land/bubbles/v2 v2.0.0-beta.1.0.20251104200223-da0b892d1759
 	charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106195925-579e174cd7fa
-	charm.land/fantasy v0.1.6
+	charm.land/fantasy v0.2.0
 	charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410
 	github.com/JohannesKaufmann/html-to-markdown v1.6.0
 	github.com/MakeNowJust/heredoc v1.0.0

go.sum 🔗

@@ -2,8 +2,8 @@ charm.land/bubbles/v2 v2.0.0-beta.1.0.20251104200223-da0b892d1759 h1:P1MxkVl8ZeI
 charm.land/bubbles/v2 v2.0.0-beta.1.0.20251104200223-da0b892d1759/go.mod h1:G7JWaj3kDT0BDB+h5BLDUhhBLpDoRLKrpOp5QrA2SQs=
 charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106195925-579e174cd7fa h1:J30WneaxF2CV3f2ofamxlr+RwyF6LKSJ3UesrtHfU9I=
 charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106195925-579e174cd7fa/go.mod h1:lUNldRH4wRBZ9SGFqlss1Pep7QXzgV/Fp+V9BsAhOPc=
-charm.land/fantasy v0.1.6 h1:laomMUqUaniQoLx7UOb+MLUpIGJPoNwsXvw1PbzgnB8=
-charm.land/fantasy v0.1.6/go.mod h1:JpFcJ5zs/1CjmYYGAZ7GaFmeBv0mPaTzEPRG6Eic5pc=
+charm.land/fantasy v0.2.0 h1:BO1eMugePqrXe46zlNQZI6ajzqt/kft0IMiIAYBqsAo=
+charm.land/fantasy v0.2.0/go.mod h1:JpFcJ5zs/1CjmYYGAZ7GaFmeBv0mPaTzEPRG6Eic5pc=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
 cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=

internal/agent/agent.go 🔗

@@ -8,6 +8,7 @@
 package agent
 
 import (
+	"cmp"
 	"context"
 	_ "embed"
 	"errors"
@@ -32,6 +33,7 @@ import (
 	"github.com/charmbracelet/crush/internal/message"
 	"github.com/charmbracelet/crush/internal/permission"
 	"github.com/charmbracelet/crush/internal/session"
+	"github.com/charmbracelet/crush/internal/stringext"
 )
 
 //go:embed templates/title.md
@@ -303,7 +305,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 			currentAssistant.AddToolCall(toolCall)
 			return a.messages.Update(genCtx, *currentAssistant)
 		},
-		OnRetry: func(err *fantasy.APICallError, delay time.Duration) {
+		OnRetry: func(err *fantasy.ProviderError, delay time.Duration) {
 			// TODO: implement
 		},
 		OnToolCall: func(tc fantasy.ToolCallContent) error {
@@ -459,12 +461,19 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 				return nil, createErr
 			}
 		}
+		var fantasyErr *fantasy.Error
+		var providerErr *fantasy.ProviderError
+		const defaultTitle = "Provider Error"
 		if isCancelErr {
 			currentAssistant.AddFinish(message.FinishReasonCanceled, "User canceled request", "")
 		} else if isPermissionErr {
 			currentAssistant.AddFinish(message.FinishReasonPermissionDenied, "User denied permission", "")
+		} else if errors.As(err, &providerErr) {
+			currentAssistant.AddFinish(message.FinishReasonError, cmp.Or(stringext.Capitalize(providerErr.Title), defaultTitle), providerErr.Message)
+		} else if errors.As(err, &fantasyErr) {
+			currentAssistant.AddFinish(message.FinishReasonError, cmp.Or(stringext.Capitalize(fantasyErr.Title), defaultTitle), fantasyErr.Message)
 		} else {
-			currentAssistant.AddFinish(message.FinishReasonError, "API Error", err.Error())
+			currentAssistant.AddFinish(message.FinishReasonError, defaultTitle, err.Error())
 		}
 		// Note: we use the parent context here because the genCtx has been
 		// cancelled.

internal/stringext/string.go 🔗

@@ -0,0 +1,10 @@
+package stringext
+
+import (
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
+)
+
+func Capitalize(text string) string {
+	return cases.Title(language.English, cases.Compact).String(text)
+}