@@ -25,6 +25,7 @@ import (
25 tea "charm.land/bubbletea/v2"
25 tea "charm.land/bubbletea/v2"
26 "charm.land/catwalk/pkg/catwalk"
26 "charm.land/catwalk/pkg/catwalk"
27 "charm.land/lipgloss/v2"
27 "charm.land/lipgloss/v2"
28+ "github.com/charmbracelet/crush/internal/agent/hyper"
28 "github.com/charmbracelet/crush/internal/agent/notify"
29 "github.com/charmbracelet/crush/internal/agent/notify"
29 agenttools "github.com/charmbracelet/crush/internal/agent/tools"
30 agenttools "github.com/charmbracelet/crush/internal/agent/tools"
30 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
31 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
@@ -151,6 +152,11 @@ type (
151 sessionFilesUpdatesMsg struct {
152 sessionFilesUpdatesMsg struct {
152 sessionFiles []SessionFile
153 sessionFiles []SessionFile
155+ // creditsUpdatedMsg is sent when the remaining Hyper credits have been
156+ // fetched from the API.
157+ creditsUpdatedMsg struct {
156// UI represents the main user interface model.
162// UI represents the main user interface model.
@@ -264,6 +270,9 @@ type UI struct {
264 // mouse highlighting related state
270 // mouse highlighting related state
265 lastClickTime time.Time
271 lastClickTime time.Time
273+ // hyperCredits is the remaining Hyper credits, updated after each prompt.
267 // Prompt history for up/down navigation through previous messages.
276 // Prompt history for up/down navigation through previous messages.
268 promptHistory struct {
277 promptHistory struct {
269 messages []string
278 messages []string
@@ -386,6 +395,9 @@ func (m *UI) Init() tea.Cmd {
386 if cmd := m.loadInitialSession(); cmd != nil {
395 if cmd := m.loadInitialSession(); cmd != nil {
387 cmds = append(cmds, cmd)
396 cmds = append(cmds, cmd)
398+ if m.com.IsHyper() {
399+ cmds = append(cmds, m.fetchHyperCredits())
389 return tea.Batch(cmds...)
401 return tea.Batch(cmds...)
@@ -854,6 +866,8 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
854 if cmd := m.handleSelectModel(msg.action); cmd != nil {
866 if cmd := m.handleSelectModel(msg.action); cmd != nil {
855 cmds = append(cmds, cmd)
867 cmds = append(cmds, cmd)
869+ case creditsUpdatedMsg:
870+ m.hyperCredits = &msg.credits
857 case util.InfoMsg:
871 case util.InfoMsg:
858 if msg.Type == util.InfoTypeError {
872 if msg.Type == util.InfoTypeError {
859 slog.Error("Error reported", "error", msg.Msg)
873 slog.Error("Error reported", "error", msg.Msg)
@@ -1575,6 +1589,33 @@ func (m *UI) refreshHyperAndRetrySelect(msg dialog.ActionSelectModel) tea.Cmd {
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()
1600+ providerCfg, ok := cfg.Providers.Get(hyper.Name)
1604+ apiKey, err := m.com.Workspace.Resolver().ResolveValue(providerCfg.APIKey)
1605+ if err != nil || apiKey == "" {
1608+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
1610+ credits, err := hyper.FetchCredits(ctx, apiKey)
1612+ slog.Error("Failed to fetch Hyper credits", "error", err)
1615+ return creditsUpdatedMsg{credits: credits}
1578// handleSelectModel performs the model selection after any provider
1619// handleSelectModel performs the model selection after any provider
1579// pre-checks (such as a silent Hyper OAuth refresh) have completed.
1620// pre-checks (such as a silent Hyper OAuth refresh) have completed.
1580func (m *UI) handleSelectModel(msg dialog.ActionSelectModel) tea.Cmd {
1621func (m *UI) handleSelectModel(msg dialog.ActionSelectModel) tea.Cmd {
@@ -1656,6 +1697,8 @@ func (m *UI) handleSelectModel(msg dialog.ActionSelectModel) tea.Cmd {
1656 if err := m.com.Workspace.InitCoderAgent(context.TODO()); err != nil {
1697 if err := m.com.Workspace.InitCoderAgent(context.TODO()); err != nil {
1657 cmds = append(cmds, util.ReportError(err))
1698 cmds = append(cmds, util.ReportError(err))
1700+ } else if m.com.IsHyper() {
1701+ cmds = append(cmds, m.fetchHyperCredits())
1661 return tea.Batch(cmds...)
1704 return tea.Batch(cmds...)
@@ -3352,10 +3395,15 @@ func (m *UI) handlePermissionNotification(notification permission.PermissionNoti
3352func (m *UI) handleAgentNotification(n notify.Notification) tea.Cmd {
3395func (m *UI) handleAgentNotification(n notify.Notification) tea.Cmd {
3353 switch n.Type {
3396 switch n.Type {
3354 case notify.TypeAgentFinished:
3397 case notify.TypeAgentFinished:
3355− return m.sendNotification(notification.Notification{
3399+ cmds = append(cmds, m.sendNotification(notification.Notification{
3356 Title: "Crush is waiting...",
3400 Title: "Crush is waiting...",
3357 Message: fmt.Sprintf("Agent's turn completed in \"%s\"", n.SessionTitle),
3401 Message: fmt.Sprintf("Agent's turn completed in \"%s\"", n.SessionTitle),
3403+ if m.com.IsHyper() {
3404+ cmds = append(cmds, m.fetchHyperCredits())
3406+ return tea.Batch(cmds...)
3359 case notify.TypeReAuthenticate:
3407 case notify.TypeReAuthenticate:
3360 return m.handleReAuthenticate(n.ProviderID)
3408 return m.handleReAuthenticate(n.ProviderID)
3361 default:
3409 default: