on main help sign in
feat: open Hyper auth dialog automatically on unauthorized error

When Hyper returns a 401, publish a TypeReAuthenticate notification so the UI opens the OAuth dialog. Also fix coordinator.isUnauthorized to recognize hyper.ErrUnauthorized for auto-refresh. ๐Ÿ’˜ Generated with Crush Assisted-by: Z.ai: GLM 5.1 via Crush <crush@charm.land>

commit59943a82e2fdb19dc0867607de851ac3411f6a83 authorAndrey Nering <andreynering@users.noreply.github.com> dateTue 7 Apr 2026, 17:04 (6 months ago) parent85b66c3
4 files changed+32โˆ’1
internal/agent/agent.go+8 internal/agent/coordinator.go+2โˆ’1 internal/agent/notify/notify.go+4 internal/ui/model/ui.go+18
internal/agent/agent.go+8
@@ -531,6 +531,14 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
531531 currentAssistant.AddFinish(message.FinishReasonPermissionDenied, "User denied permission", "")
532532 } else if errors.Is(err, hyper.ErrUnauthorized) {
533533 currentAssistant.AddFinish(message.FinishReasonError, "Unauthorized", `Authentication with Hyper failed. Please run "crush auth" to re-authenticate.`)
534+ if a.notify != nil {
535+ a.notify.Publish(pubsub.CreatedEvent, notify.Notification{
536+ SessionID: call.SessionID,
537+ SessionTitle: currentSession.Title,
538+ Type: notify.TypeReAuthenticate,
539+ ProviderID: largeModel.ModelCfg.Provider,
540+ })
541+ }
534542 } else if errors.Is(err, hyper.ErrNoCredits) {
535543 url := hyper.BaseURL()
536544 link := linkStyle.Hyperlink(url, "id=hyper").Render(url)
internal/agent/coordinator.go+2โˆ’1
@@ -921,7 +921,8 @@ func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
921921
922922func (c *coordinator) isUnauthorized(err error) bool {
923923 var providerErr *fantasy.ProviderError
924โˆ’ return errors.As(err, &providerErr) && providerErr.StatusCode == http.StatusUnauthorized
924+ return (errors.As(err, &providerErr) && providerErr.StatusCode == http.StatusUnauthorized) ||
925+ errors.Is(err, hyper.ErrUnauthorized)
925926}
926927
927928func (c *coordinator) refreshOAuth2Token(ctx context.Context, providerCfg config.ProviderConfig) error {
internal/agent/notify/notify.go+4
@@ -9,6 +9,9 @@ type Type string
99const (
1010 // TypeAgentFinished indicates the agent has completed its turn.
1111 TypeAgentFinished Type = "agent_finished"
12+ // TypeReAuthenticate indicates the agent encountered an
13+ // authentication error and the user needs to re-authenticate.
14+ TypeReAuthenticate Type = "re_authenticate"
1215)
1316
1417// Notification represents a domain event published by the agent.
@@ -16,4 +19,5 @@ type Notification struct {
1619 SessionID string
1720 SessionTitle string
1821 Type Type
22+ ProviderID string
1923}
internal/ui/model/ui.go+18
@@ -3203,11 +3203,29 @@ func (m *UI) handleAgentNotification(n notify.Notification) tea.Cmd {
32033203 Title: "Crush is waiting...",
32043204 Message: fmt.Sprintf("Agent's turn completed in \"%s\"", n.SessionTitle),
32053205 })
3206+ case notify.TypeReAuthenticate:
3207+ return m.handleReAuthenticate(n.ProviderID)
32063208 default:
32073209 return nil
32083210 }
32093211}
32103212
3213+func (m *UI) handleReAuthenticate(providerID string) tea.Cmd {
3214+ cfg := m.com.Config()
3215+ if cfg == nil {
3216+ return nil
3217+ }
3218+ providerCfg, ok := cfg.Providers.Get(providerID)
3219+ if !ok {
3220+ return nil
3221+ }
3222+ agentCfg, ok := cfg.Agents[config.AgentCoder]
3223+ if !ok {
3224+ return nil
3225+ }
3226+ return m.openAuthenticationDialog(providerCfg.ToProvider(), cfg.Models[agentCfg.Model], agentCfg.Model)
3227+}
3228+
32113229// newSession clears the current session state and prepares for a new session.
32123230// The actual session creation happens when the user sends their first message.
32133231// Returns a command to reload prompt history.