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	// ActionInitializeProject is a message to initialize a project.
 55	ActionInitializeProject struct{}
 56	ActionSummarize         struct {
 57		SessionID string
 58	}
 59	// ActionSelectReasoningEffort is a message indicating a reasoning effort has been selected.
 60	ActionSelectReasoningEffort struct {
 61		Effort string
 62	}
 63	ActionPermissionResponse struct {
 64		Permission permission.PermissionRequest
 65		Action     PermissionAction
 66	}
 67	// ActionRunCustomCommand is a message to run a custom command.
 68	ActionRunCustomCommand struct {
 69		Content   string
 70		Arguments []commands.Argument
 71		Args      map[string]string // Actual argument values
 72	}
 73	// ActionRunMCPPrompt is a message to run a custom command.
 74	ActionRunMCPPrompt struct {
 75		Title       string
 76		Description string
 77		PromptID    string
 78		ClientID    string
 79		Arguments   []commands.Argument
 80		Args        map[string]string // Actual argument values
 81	}
 82)
 83
 84// Messages for API key input dialog.
 85type (
 86	ActionChangeAPIKeyState struct {
 87		State APIKeyInputState
 88	}
 89)
 90
 91// Messages for OAuth2 device flow dialog.
 92type (
 93	// ActionInitiateOAuth is sent when the device auth is initiated
 94	// successfully.
 95	ActionInitiateOAuth struct {
 96		DeviceCode      string
 97		UserCode        string
 98		ExpiresIn       int
 99		VerificationURL string
100		Interval        int
101	}
102
103	// ActionCompleteOAuth is sent when the device flow completes successfully.
104	ActionCompleteOAuth struct {
105		Token *oauth.Token
106	}
107
108	// ActionOAuthErrored is sent when the device flow encounters an error.
109	ActionOAuthErrored struct {
110		Error error
111	}
112)
113
114// ActionCmd represents an action that carries a [tea.Cmd] to be passed to the
115// Bubble Tea program loop.
116type ActionCmd struct {
117	Cmd tea.Cmd
118}
119
120// ActionFilePickerSelected is a message indicating a file has been selected in
121// the file picker dialog.
122type ActionFilePickerSelected struct {
123	Path string
124}
125
126// Cmd returns a command that reads the file at path and sends a
127// [message.Attachement] to the program.
128func (a ActionFilePickerSelected) Cmd() tea.Cmd {
129	path := a.Path
130	if path == "" {
131		return nil
132	}
133	return func() tea.Msg {
134		isFileLarge, err := common.IsFileTooBig(path, common.MaxAttachmentSize)
135		if err != nil {
136			return util.InfoMsg{
137				Type: util.InfoTypeError,
138				Msg:  fmt.Sprintf("unable to read the image: %v", err),
139			}
140		}
141		if isFileLarge {
142			return util.InfoMsg{
143				Type: util.InfoTypeError,
144				Msg:  "file too large, max 5MB",
145			}
146		}
147
148		content, err := os.ReadFile(path)
149		if err != nil {
150			return util.InfoMsg{
151				Type: util.InfoTypeError,
152				Msg:  fmt.Sprintf("unable to read the image: %v", err),
153			}
154		}
155
156		mimeBufferSize := min(512, len(content))
157		mimeType := http.DetectContentType(content[:mimeBufferSize])
158		fileName := filepath.Base(path)
159
160		return message.Attachment{
161			FilePath: path,
162			FileName: fileName,
163			MimeType: mimeType,
164			Content:  content,
165		}
166	}
167}