actions.go

  1package dialog
  2
  3import (
  4	"fmt"
  5	"net/http"
  6	"os"
  7	"path/filepath"
  8
  9	tea "charm.land/bubbletea/v2"
 10	"charm.land/catwalk/pkg/catwalk"
 11	"github.com/charmbracelet/crush/internal/commands"
 12	"github.com/charmbracelet/crush/internal/config"
 13	"github.com/charmbracelet/crush/internal/message"
 14	"github.com/charmbracelet/crush/internal/oauth"
 15	"github.com/charmbracelet/crush/internal/permission"
 16	"github.com/charmbracelet/crush/internal/session"
 17	"github.com/charmbracelet/crush/internal/ui/common"
 18	"github.com/charmbracelet/crush/internal/ui/util"
 19)
 20
 21// ActionClose is a message to close the current dialog.
 22type ActionClose struct{}
 23
 24// ActionQuit is a message to quit the application.
 25type ActionQuit = tea.QuitMsg
 26
 27// ActionOpenDialog is a message to open a dialog.
 28type ActionOpenDialog struct {
 29	DialogID string
 30}
 31
 32// ActionSelectSession is a message indicating a session has been selected.
 33type ActionSelectSession struct {
 34	Session session.Session
 35}
 36
 37// ActionSelectModel is a message indicating a model has been selected.
 38type ActionSelectModel struct {
 39	Provider       catwalk.Provider
 40	Model          config.SelectedModel
 41	ModelType      config.SelectedModelType
 42	ReAuthenticate bool
 43}
 44
 45// Messages for commands
 46type (
 47	ActionNewSession          struct{}
 48	ActionToggleHelp          struct{}
 49	ActionToggleCompactMode   struct{}
 50	ActionToggleThinking      struct{}
 51	ActionTogglePills         struct{}
 52	ActionExternalEditor      struct{}
 53	ActionToggleYoloMode      struct{}
 54	ActionToggleNotifications struct{}
 55	// ActionInitializeProject is a message to initialize a project.
 56	ActionInitializeProject struct{}
 57	ActionSummarize         struct {
 58		SessionID string
 59	}
 60	// ActionSelectReasoningEffort is a message indicating a reasoning effort has been selected.
 61	ActionSelectReasoningEffort struct {
 62		Effort string
 63	}
 64	ActionPermissionResponse struct {
 65		Permission permission.PermissionRequest
 66		Action     PermissionAction
 67	}
 68	// ActionRunCustomCommand is a message to run a custom command.
 69	ActionRunCustomCommand struct {
 70		Content   string
 71		Arguments []commands.Argument
 72		Args      map[string]string // Actual argument values
 73	}
 74	// ActionRunMCPPrompt is a message to run a custom command.
 75	ActionRunMCPPrompt struct {
 76		Title       string
 77		Description string
 78		PromptID    string
 79		ClientID    string
 80		Arguments   []commands.Argument
 81		Args        map[string]string // Actual argument values
 82	}
 83)
 84
 85// Messages for API key input dialog.
 86type (
 87	ActionChangeAPIKeyState struct {
 88		State APIKeyInputState
 89	}
 90)
 91
 92// Messages for OAuth2 device flow dialog.
 93type (
 94	// ActionInitiateOAuth is sent when the device auth is initiated
 95	// successfully.
 96	ActionInitiateOAuth struct {
 97		DeviceCode      string
 98		UserCode        string
 99		ExpiresIn       int
100		VerificationURL string
101		Interval        int
102	}
103
104	// ActionCompleteOAuth is sent when the device flow completes successfully.
105	ActionCompleteOAuth struct {
106		Token *oauth.Token
107	}
108
109	// ActionOAuthErrored is sent when the device flow encounters an error.
110	ActionOAuthErrored struct {
111		Error error
112	}
113)
114
115// ActionCmd represents an action that carries a [tea.Cmd] to be passed to the
116// Bubble Tea program loop.
117type ActionCmd struct {
118	Cmd tea.Cmd
119}
120
121// ActionFilePickerSelected is a message indicating a file has been selected in
122// the file picker dialog.
123type ActionFilePickerSelected struct {
124	Path string
125}
126
127// Cmd returns a command that reads the file at path and sends a
128// [message.Attachement] to the program.
129func (a ActionFilePickerSelected) Cmd() tea.Cmd {
130	path := a.Path
131	if path == "" {
132		return nil
133	}
134	return func() tea.Msg {
135		isFileLarge, err := common.IsFileTooBig(path, common.MaxAttachmentSize)
136		if err != nil {
137			return util.InfoMsg{
138				Type: util.InfoTypeError,
139				Msg:  fmt.Sprintf("unable to read the image: %v", err),
140			}
141		}
142		if isFileLarge {
143			return util.InfoMsg{
144				Type: util.InfoTypeError,
145				Msg:  "file too large, max 5MB",
146			}
147		}
148
149		content, err := os.ReadFile(path)
150		if err != nil {
151			return util.InfoMsg{
152				Type: util.InfoTypeError,
153				Msg:  fmt.Sprintf("unable to read the image: %v", err),
154			}
155		}
156
157		mimeBufferSize := min(512, len(content))
158		mimeType := http.DetectContentType(content[:mimeBufferSize])
159		fileName := filepath.Base(path)
160
161		return message.Attachment{
162			FilePath: path,
163			FileName: fileName,
164			MimeType: mimeType,
165			Content:  content,
166		}
167	}
168}