@@ -0,0 +1,297 @@
6+ "charm.land/bubbles/v2/help"
7+ "charm.land/bubbles/v2/key"
8+ "charm.land/bubbles/v2/textinput"
9+ tea "charm.land/bubbletea/v2"
10+ "github.com/charmbracelet/crush/internal/config"
11+ "github.com/charmbracelet/crush/internal/ui/common"
12+ "github.com/charmbracelet/crush/internal/ui/list"
13+ "github.com/charmbracelet/crush/internal/ui/styles"
14+ uv "github.com/charmbracelet/ultraviolet"
15+ "github.com/sahilm/fuzzy"
16+ "golang.org/x/text/cases"
17+ "golang.org/x/text/language"
21+ // ReasoningID is the identifier for the reasoning effort dialog.
22+ ReasoningID = "reasoning"
23+ reasoningDialogMaxWidth = 80
24+ reasoningDialogMaxHeight = 12
27+// Reasoning represents a dialog for selecting reasoning effort.
28+type Reasoning struct {
31+ list *list.FilterableList
32+ input textinput.Model
43+// ReasoningItem represents a reasoning effort list item.
44+type ReasoningItem struct {
55+ _ Dialog = (*Reasoning)(nil)
56+ _ ListItem = (*ReasoningItem)(nil)
59+// NewReasoning creates a new reasoning effort dialog.
60+func NewReasoning(com *common.Common) (*Reasoning, error) {
61+ r := &Reasoning{com: com}
64+ help.Styles = com.Styles.DialogHelpStyles()
67+ r.list = list.NewFilterableList()
70+ r.input = textinput.New()
71+ r.input.SetVirtualCursor(false)
72+ r.input.Placeholder = "Type to filter"
73+ r.input.SetStyles(com.Styles.TextInput)
76+ r.keyMap.Select = key.NewBinding(
77+ key.WithKeys("enter", "ctrl+y"),
78+ key.WithHelp("enter", "confirm"),
80+ r.keyMap.Next = key.NewBinding(
81+ key.WithKeys("down", "ctrl+n"),
82+ key.WithHelp("↓", "next item"),
84+ r.keyMap.Previous = key.NewBinding(
85+ key.WithKeys("up", "ctrl+p"),
86+ key.WithHelp("↑", "previous item"),
88+ r.keyMap.UpDown = key.NewBinding(
89+ key.WithKeys("up", "down"),
90+ key.WithHelp("↑/↓", "choose"),
92+ r.keyMap.Close = CloseKey
94+ if err := r.setReasoningItems(); err != nil {
101+// ID implements Dialog.
102+func (r *Reasoning) ID() string {
106+// HandleMsg implements [Dialog].
107+func (r *Reasoning) HandleMsg(msg tea.Msg) Action {
108+ switch msg := msg.(type) {
109+ case tea.KeyPressMsg:
111+ case key.Matches(msg, r.keyMap.Close):
112+ return ActionClose{}
113+ case key.Matches(msg, r.keyMap.Previous):
115+ if r.list.IsSelectedFirst() {
117+ r.list.ScrollToBottom()
121+ r.list.ScrollToSelected()
122+ case key.Matches(msg, r.keyMap.Next):
124+ if r.list.IsSelectedLast() {
125+ r.list.SelectFirst()
126+ r.list.ScrollToTop()
130+ r.list.ScrollToSelected()
131+ case key.Matches(msg, r.keyMap.Select):
132+ selectedItem := r.list.SelectedItem()
133+ if selectedItem == nil {
136+ reasoningItem, ok := selectedItem.(*ReasoningItem)
140+ return ActionSelectReasoningEffort{Effort: reasoningItem.effort}
143+ r.input, cmd = r.input.Update(msg)
144+ value := r.input.Value()
145+ r.list.SetFilter(value)
146+ r.list.ScrollToTop()
147+ r.list.SetSelected(0)
148+ return ActionCmd{cmd}
154+// Cursor returns the cursor position relative to the dialog.
155+func (r *Reasoning) Cursor() *tea.Cursor {
156+ return InputCursor(r.com.Styles, r.input.Cursor())
159+// Draw implements [Dialog].
160+func (r *Reasoning) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
162+ width := max(0, min(reasoningDialogMaxWidth, area.Dx()))
163+ height := max(0, min(reasoningDialogMaxHeight, area.Dy()))
164+ innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
165+ heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
166+ t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
167+ t.Dialog.HelpView.GetVerticalFrameSize() +
168+ t.Dialog.View.GetVerticalFrameSize()
170+ r.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1)
171+ r.list.SetSize(innerWidth, height-heightOffset)
172+ r.help.SetWidth(innerWidth)
174+ rc := NewRenderContext(t, width)
175+ rc.Title = "Select Reasoning Effort"
176+ inputView := t.Dialog.InputPrompt.Render(r.input.View())
177+ rc.AddPart(inputView)
179+ visibleCount := len(r.list.VisibleItems())
180+ if r.list.Height() >= visibleCount {
181+ r.list.ScrollToTop()
183+ r.list.ScrollToSelected()
186+ listView := t.Dialog.List.Height(r.list.Height()).Render(r.list.Render())
187+ rc.AddPart(listView)
188+ rc.Help = r.help.View(r)
193+ DrawCenterCursor(scr, area, view, cur)
197+// ShortHelp implements [help.KeyMap].
198+func (r *Reasoning) ShortHelp() []key.Binding {
199+ return []key.Binding{
206+// FullHelp implements [help.KeyMap].
207+func (r *Reasoning) FullHelp() [][]key.Binding {
208+ m := [][]key.Binding{}
209+ slice := []key.Binding{
215+ for i := 0; i < len(slice); i += 4 {
216+ end := min(i+4, len(slice))
217+ m = append(m, slice[i:end])
222+func (r *Reasoning) setReasoningItems() error {
223+ cfg := r.com.Config()
224+ agentCfg, ok := cfg.Agents[config.AgentCoder]
226+ return errors.New("agent configuration not found")
229+ selectedModel := cfg.Models[agentCfg.Model]
230+ model := cfg.GetModelByType(agentCfg.Model)
232+ return errors.New("model configuration not found")
235+ if len(model.ReasoningLevels) == 0 {
236+ return errors.New("no reasoning levels available")
239+ currentEffort := selectedModel.ReasoningEffort
240+ if currentEffort == "" {
241+ currentEffort = model.DefaultReasoningEffort
244+ caser := cases.Title(language.English)
245+ items := make([]list.FilterableItem, 0, len(model.ReasoningLevels))
247+ for i, effort := range model.ReasoningLevels {
248+ item := &ReasoningItem{
250+ title: caser.String(effort),
251+ isCurrent: effort == currentEffort,
254+ items = append(items, item)
255+ if effort == currentEffort {
260+ r.list.SetItems(items...)
261+ r.list.SetSelected(selectedIndex)
262+ r.list.ScrollToSelected()
266+// Filter returns the filter value for the reasoning item.
267+func (r *ReasoningItem) Filter() string {
271+// ID returns the unique identifier for the reasoning effort.
272+func (r *ReasoningItem) ID() string {
276+// SetFocused sets the focus state of the reasoning item.
277+func (r *ReasoningItem) SetFocused(focused bool) {
278+ if r.focused != focused {
284+// SetMatch sets the fuzzy match for the reasoning item.
285+func (r *ReasoningItem) SetMatch(m fuzzy.Match) {
290+// Render returns the string representation of the reasoning item.
291+func (r *ReasoningItem) Render(width int) string {
296+ return renderItem(r.t, r.title, info, r.focused, width, r.cache, &r.m)