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