actions.go

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