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