@@ -0,0 +1,268 @@
4+ "github.com/charmbracelet/bubbles/v2/help"
5+ "github.com/charmbracelet/bubbles/v2/key"
6+ tea "github.com/charmbracelet/bubbletea/v2"
7+ "github.com/charmbracelet/lipgloss/v2"
9+ "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/crush/internal/tui/components/core"
11+ "github.com/charmbracelet/crush/internal/tui/components/dialogs"
12+ "github.com/charmbracelet/crush/internal/tui/exp/list"
13+ "github.com/charmbracelet/crush/internal/tui/styles"
14+ "github.com/charmbracelet/crush/internal/tui/util"
18+ ReasoningDialogID dialogs.DialogID = "reasoning"
20+ defaultWidth int = 50
23+type listModel = list.FilterableList[list.CompletionItem[EffortOption]]
25+type EffortOption struct {
30+type ReasoningDialog interface {
34+type reasoningDialogCmp struct {
36+ wWidth int // Width of the terminal window
37+ wHeight int // Height of the terminal window
40+ keyMap ReasoningDialogKeyMap
44+type ReasoningEffortSelectedMsg struct {
48+type ReasoningDialogKeyMap struct {
55+func DefaultReasoningDialogKeyMap() ReasoningDialogKeyMap {
56+ return ReasoningDialogKeyMap{
57+ Next: key.NewBinding(
58+ key.WithKeys("down", "j", "ctrl+n"),
59+ key.WithHelp("↓/j/ctrl+n", "next"),
61+ Previous: key.NewBinding(
62+ key.WithKeys("up", "k", "ctrl+p"),
63+ key.WithHelp("↑/k/ctrl+p", "previous"),
65+ Select: key.NewBinding(
66+ key.WithKeys("enter"),
67+ key.WithHelp("enter", "select"),
69+ Close: key.NewBinding(
70+ key.WithKeys("esc", "ctrl+c"),
71+ key.WithHelp("esc/ctrl+c", "close"),
76+func (k ReasoningDialogKeyMap) ShortHelp() []key.Binding {
77+ return []key.Binding{k.Select, k.Close}
80+func (k ReasoningDialogKeyMap) FullHelp() [][]key.Binding {
81+ return [][]key.Binding{
82+ {k.Next, k.Previous},
87+func NewReasoningDialog() ReasoningDialog {
88+ keyMap := DefaultReasoningDialogKeyMap()
89+ listKeyMap := list.DefaultKeyMap()
90+ listKeyMap.Down.SetEnabled(false)
91+ listKeyMap.Up.SetEnabled(false)
92+ listKeyMap.DownOneItem = keyMap.Next
93+ listKeyMap.UpOneItem = keyMap.Previous
95+ t := styles.CurrentTheme()
96+ inputStyle := t.S().Base.PaddingLeft(1).PaddingBottom(1)
97+ effortList := list.NewFilterableList(
98+ []list.CompletionItem[EffortOption]{},
99+ list.WithFilterInputStyle(inputStyle),
100+ list.WithFilterListOptions(
101+ list.WithKeyMap(listKeyMap),
102+ list.WithWrapNavigation(),
103+ list.WithResizeByList(),
107+ help.Styles = t.S().Help
109+ return &reasoningDialogCmp{
110+ effortList: effortList,
111+ width: defaultWidth,
117+func (r *reasoningDialogCmp) Init() tea.Cmd {
118+ return r.populateEffortOptions()
121+func (r *reasoningDialogCmp) populateEffortOptions() tea.Cmd {
123+ if agentCfg, ok := cfg.Agents["coder"]; ok {
124+ selectedModel := cfg.Models[agentCfg.Model]
125+ model := cfg.GetModelByType(agentCfg.Model)
127+ // Get current reasoning effort
128+ currentEffort := selectedModel.ReasoningEffort
129+ if currentEffort == "" && model != nil {
130+ currentEffort = model.DefaultReasoningEffort
133+ efforts := []EffortOption{
148+ effortItems := []list.CompletionItem[EffortOption]{}
150+ for _, effort := range efforts {
151+ opts := []list.CompletionItemOption{
152+ list.WithCompletionID(effort.Effort),
154+ if effort.Effort == currentEffort {
155+ opts = append(opts, list.WithCompletionShortcut("current"))
156+ selectedID = effort.Effort
158+ effortItems = append(effortItems, list.NewCompletionItem(
165+ cmd := r.effortList.SetItems(effortItems)
166+ // Set the current effort as the selected item
167+ if currentEffort != "" && selectedID != "" {
168+ return tea.Sequence(cmd, r.effortList.SetSelected(selectedID))
175+func (r *reasoningDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
176+ switch msg := msg.(type) {
177+ case tea.WindowSizeMsg:
178+ r.wWidth = msg.Width
179+ r.wHeight = msg.Height
180+ return r, r.effortList.SetSize(r.listWidth(), r.listHeight())
181+ case tea.KeyPressMsg:
183+ case key.Matches(msg, r.keyMap.Select):
184+ selectedItem := r.effortList.SelectedItem()
185+ if selectedItem == nil {
186+ return r, nil // No item selected, do nothing
188+ effort := (*selectedItem).Value()
189+ return r, tea.Sequence(
190+ util.CmdHandler(dialogs.CloseDialogMsg{}),
192+ return ReasoningEffortSelectedMsg{
193+ Effort: effort.Effort,
197+ case key.Matches(msg, r.keyMap.Close):
198+ return r, util.CmdHandler(dialogs.CloseDialogMsg{})
200+ u, cmd := r.effortList.Update(msg)
201+ r.effortList = u.(listModel)
208+func (r *reasoningDialogCmp) View() string {
209+ t := styles.CurrentTheme()
210+ listView := r.effortList
212+ header := t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Select Reasoning Effort", r.width-4))
213+ content := lipgloss.JoinVertical(
218+ t.S().Base.Width(r.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(r.help.View(r.keyMap)),
220+ return r.style().Render(content)
223+func (r *reasoningDialogCmp) Cursor() *tea.Cursor {
224+ if cursor, ok := r.effortList.(util.Cursor); ok {
225+ cursor := cursor.Cursor()
227+ cursor = r.moveCursor(cursor)
234+func (r *reasoningDialogCmp) listWidth() int {
235+ return r.width - 2 // 4 for padding
238+func (r *reasoningDialogCmp) listHeight() int {
239+ listHeight := len(r.effortList.Items()) + 2 + 4 // height based on items + 2 for the input + 4 for the sections
240+ return min(listHeight, r.wHeight/2)
243+func (r *reasoningDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
244+ row, col := r.Position()
247+ cursor.X = cursor.X + col + 2
251+func (r *reasoningDialogCmp) style() lipgloss.Style {
252+ t := styles.CurrentTheme()
255+ Border(lipgloss.RoundedBorder()).
256+ BorderForeground(t.BorderFocus)
259+func (r *reasoningDialogCmp) Position() (int, int) {
260+ row := r.wHeight/4 - 2 // just a bit above the center
266+func (r *reasoningDialogCmp) ID() dialogs.DialogID {
267+ return ReasoningDialogID