@@ -25,6 +25,7 @@ import (
2525 tea "charm.land/bubbletea/v2"
2626 "charm.land/catwalk/pkg/catwalk"
2727 "charm.land/lipgloss/v2"
28+ "github.com/charmbracelet/crush/internal/agent/hyper"
2829 "github.com/charmbracelet/crush/internal/agent/notify"
2930 agenttools "github.com/charmbracelet/crush/internal/agent/tools"
3031 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
@@ -151,6 +152,11 @@ type (
151152 sessionFilesUpdatesMsg struct {
152153 sessionFiles []SessionFile
153154 }
155+ // creditsUpdatedMsg is sent when the remaining Hyper credits have been
156+ // fetched from the API.
157+ creditsUpdatedMsg struct {
158+ credits int
159+ }
154160)
155161
156162// UI represents the main user interface model.
@@ -264,6 +270,9 @@ type UI struct {
264270 // mouse highlighting related state
265271 lastClickTime time.Time
266272
273+ // hyperCredits is the remaining Hyper credits, updated after each prompt.
274+ hyperCredits *int
275+
267276 // Prompt history for up/down navigation through previous messages.
268277 promptHistory struct {
269278 messages []string
@@ -386,6 +395,9 @@ func (m *UI) Init() tea.Cmd {
386395 if cmd := m.loadInitialSession(); cmd != nil {
387396 cmds = append(cmds, cmd)
388397 }
398+ if m.com.IsHyper() {
399+ cmds = append(cmds, m.fetchHyperCredits())
400+ }
389401 return tea.Batch(cmds...)
390402}
391403
@@ -854,6 +866,8 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
854866 if cmd := m.handleSelectModel(msg.action); cmd != nil {
855867 cmds = append(cmds, cmd)
856868 }
869+ case creditsUpdatedMsg:
870+ m.hyperCredits = &msg.credits
857871 case util.InfoMsg:
858872 if msg.Type == util.InfoTypeError {
859873 slog.Error("Error reported", "error", msg.Msg)
@@ -1575,6 +1589,33 @@ func (m *UI) refreshHyperAndRetrySelect(msg dialog.ActionSelectModel) tea.Cmd {
15751589 }
15761590}
15771591
1592+// fetchHyperCredits returns a command that asynchronously fetches the
1593+// remaining Hyper credits from the API.
1594+func (m *UI) fetchHyperCredits() tea.Cmd {
1595+ return func() tea.Msg {
1596+ cfg := m.com.Config()
1597+ if cfg == nil {
1598+ return nil
1599+ }
1600+ providerCfg, ok := cfg.Providers.Get(hyper.Name)
1601+ if !ok {
1602+ return nil
1603+ }
1604+ apiKey, err := m.com.Workspace.Resolver().ResolveValue(providerCfg.APIKey)
1605+ if err != nil || apiKey == "" {
1606+ return nil
1607+ }
1608+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
1609+ defer cancel()
1610+ credits, err := hyper.FetchCredits(ctx, apiKey)
1611+ if err != nil {
1612+ slog.Error("Failed to fetch Hyper credits", "error", err)
1613+ return nil
1614+ }
1615+ return creditsUpdatedMsg{credits: credits}
1616+ }
1617+}
1618+
15781619// handleSelectModel performs the model selection after any provider
15791620// pre-checks (such as a silent Hyper OAuth refresh) have completed.
15801621func (m *UI) handleSelectModel(msg dialog.ActionSelectModel) tea.Cmd {
@@ -1656,6 +1697,8 @@ func (m *UI) handleSelectModel(msg dialog.ActionSelectModel) tea.Cmd {
16561697 if err := m.com.Workspace.InitCoderAgent(context.TODO()); err != nil {
16571698 cmds = append(cmds, util.ReportError(err))
16581699 }
1700+ } else if m.com.IsHyper() {
1701+ cmds = append(cmds, m.fetchHyperCredits())
16591702 }
16601703
16611704 return tea.Batch(cmds...)
@@ -3352,10 +3395,15 @@ func (m *UI) handlePermissionNotification(notification permission.PermissionNoti
33523395func (m *UI) handleAgentNotification(n notify.Notification) tea.Cmd {
33533396 switch n.Type {
33543397 case notify.TypeAgentFinished:
3355− return m.sendNotification(notification.Notification{
3398+ var cmds []tea.Cmd
3399+ cmds = append(cmds, m.sendNotification(notification.Notification{
33563400 Title: "Crush is waiting...",
33573401 Message: fmt.Sprintf("Agent's turn completed in \"%s\"", n.SessionTitle),
3358− })
3402+ }))
3403+ if m.com.IsHyper() {
3404+ cmds = append(cmds, m.fetchHyperCredits())
3405+ }
3406+ return tea.Batch(cmds...)
33593407 case notify.TypeReAuthenticate:
33603408 return m.handleReAuthenticate(n.ProviderID)
33613409 default: