From 59943a82e2fdb19dc0867607de851ac3411f6a83 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Tue, 7 Apr 2026 17:04:59 -0300 Subject: [PATCH] feat: open Hyper auth dialog automatically on unauthorized error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/agent/agent.go | 8 ++++++++ internal/agent/coordinator.go | 3 ++- internal/agent/notify/notify.go | 4 ++++ internal/ui/model/ui.go | 18 ++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 915012d810ba8035f7c2ade7a85a249f200da19e..df869af718d0b16905e71a3c9f5b03800b3fc9a8 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -531,6 +531,14 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy currentAssistant.AddFinish(message.FinishReasonPermissionDenied, "User denied permission", "") } else if errors.Is(err, hyper.ErrUnauthorized) { currentAssistant.AddFinish(message.FinishReasonError, "Unauthorized", `Authentication with Hyper failed. Please run "crush auth" to re-authenticate.`) + if a.notify != nil { + a.notify.Publish(pubsub.CreatedEvent, notify.Notification{ + SessionID: call.SessionID, + SessionTitle: currentSession.Title, + Type: notify.TypeReAuthenticate, + ProviderID: largeModel.ModelCfg.Provider, + }) + } } else if errors.Is(err, hyper.ErrNoCredits) { url := hyper.BaseURL() link := linkStyle.Hyperlink(url, "id=hyper").Render(url) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index a64029e910e0b2ce77ae2eb2e53ab4ad589c0f99..59401896d00c46c4fca25d423687969bd5d5191a 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -921,7 +921,8 @@ func (c *coordinator) Summarize(ctx context.Context, sessionID string) error { func (c *coordinator) isUnauthorized(err error) bool { var providerErr *fantasy.ProviderError - return errors.As(err, &providerErr) && providerErr.StatusCode == http.StatusUnauthorized + return (errors.As(err, &providerErr) && providerErr.StatusCode == http.StatusUnauthorized) || + errors.Is(err, hyper.ErrUnauthorized) } func (c *coordinator) refreshOAuth2Token(ctx context.Context, providerCfg config.ProviderConfig) error { diff --git a/internal/agent/notify/notify.go b/internal/agent/notify/notify.go index aba0069a1dc945dd42dd8f6a513095fa8d14157e..2ffb03203dd36f646cad6c544c717c400917b007 100644 --- a/internal/agent/notify/notify.go +++ b/internal/agent/notify/notify.go @@ -9,6 +9,9 @@ type Type string const ( // TypeAgentFinished indicates the agent has completed its turn. TypeAgentFinished Type = "agent_finished" + // TypeReAuthenticate indicates the agent encountered an + // authentication error and the user needs to re-authenticate. + TypeReAuthenticate Type = "re_authenticate" ) // Notification represents a domain event published by the agent. @@ -16,4 +19,5 @@ type Notification struct { SessionID string SessionTitle string Type Type + ProviderID string } diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 75773cf4f3bdf065e6aa4fa71f868443e546cb51..1619d3f60e2435e416f0c784c79ca14cd3b4d969 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -3203,11 +3203,29 @@ func (m *UI) handleAgentNotification(n notify.Notification) tea.Cmd { Title: "Crush is waiting...", Message: fmt.Sprintf("Agent's turn completed in \"%s\"", n.SessionTitle), }) + case notify.TypeReAuthenticate: + return m.handleReAuthenticate(n.ProviderID) default: return nil } } +func (m *UI) handleReAuthenticate(providerID string) tea.Cmd { + cfg := m.com.Config() + if cfg == nil { + return nil + } + providerCfg, ok := cfg.Providers.Get(providerID) + if !ok { + return nil + } + agentCfg, ok := cfg.Agents[config.AgentCoder] + if !ok { + return nil + } + return m.openAuthenticationDialog(providerCfg.ToProvider(), cfg.Models[agentCfg.Model], agentCfg.Model) +} + // newSession clears the current session state and prepares for a new session. // The actual session creation happens when the user sends their first message. // Returns a command to reload prompt history.