Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit 1b178f5

Browse files
committed
sessions dialog
1 parent dff1bac commit 1b178f5

12 files changed

Lines changed: 436 additions & 72 deletions

File tree

internal/tui/components/completions/completions.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,9 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
133133
c.x = msg.X
134134
c.y = msg.Y
135135
items := []util.Model{}
136+
t := styles.CurrentTheme()
136137
for _, completion := range msg.Completions {
137-
item := NewCompletionItem(completion.Title, completion.Value)
138+
item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
138139
items = append(items, item)
139140
}
140141
c.height = max(min(10, len(items)), 1) // Ensure at least 1 item height

internal/tui/components/completions/item.go

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package completions
22

33
import (
4+
"image/color"
5+
46
tea "github.com/charmbracelet/bubbletea/v2"
57
"github.com/charmbracelet/lipgloss/v2"
68
"github.com/charmbracelet/x/ansi"
79
"github.com/opencode-ai/opencode/internal/tui/components/core/list"
810
"github.com/opencode-ai/opencode/internal/tui/layout"
911
"github.com/opencode-ai/opencode/internal/tui/styles"
10-
"github.com/opencode-ai/opencode/internal/tui/theme"
1112
"github.com/opencode-ai/opencode/internal/tui/util"
1213
"github.com/rivo/uniseg"
1314
)
@@ -27,16 +28,35 @@ type completionItemCmp struct {
2728
value any
2829
focus bool
2930
matchIndexes []int
31+
bgColor color.Color
32+
}
33+
34+
type completionOptions func(*completionItemCmp)
35+
36+
func WithBackgroundColor(c color.Color) completionOptions {
37+
return func(cmp *completionItemCmp) {
38+
cmp.bgColor = c
39+
}
3040
}
3141

32-
func NewCompletionItem(text string, value any, matchIndexes ...int) CompletionItem {
33-
return &completionItemCmp{
34-
text: text,
35-
value: value,
36-
matchIndexes: matchIndexes,
42+
func WithMatchIndexes(indexes ...int) completionOptions {
43+
return func(cmp *completionItemCmp) {
44+
cmp.matchIndexes = indexes
3745
}
3846
}
3947

48+
func NewCompletionItem(text string, value any, opts ...completionOptions) CompletionItem {
49+
c := &completionItemCmp{
50+
text: text,
51+
value: value,
52+
}
53+
54+
for _, opt := range opts {
55+
opt(c)
56+
}
57+
return c
58+
}
59+
4060
// Init implements CommandItem.
4161
func (c *completionItemCmp) Init() tea.Cmd {
4262
return nil
@@ -49,15 +69,18 @@ func (c *completionItemCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
4969

5070
// View implements CommandItem.
5171
func (c *completionItemCmp) View() tea.View {
52-
t := theme.CurrentTheme()
72+
t := styles.CurrentTheme()
5373

54-
baseStyle := styles.BaseStyle().Background(t.BackgroundSecondary())
55-
titleStyle := baseStyle.Padding(0, 1).Width(c.width).Foreground(t.Text())
56-
titleMatchStyle := baseStyle.Foreground(t.Text()).Underline(true)
74+
titleStyle := t.S().Text.Padding(0, 1).Width(c.width)
75+
titleMatchStyle := t.S().Text.Underline(true)
76+
if c.bgColor != nil {
77+
titleStyle = titleStyle.Background(c.bgColor)
78+
titleMatchStyle = titleMatchStyle.Background(c.bgColor)
79+
}
5780

5881
if c.focus {
59-
titleStyle = titleStyle.Foreground(t.Background()).Background(t.Primary()).Bold(true)
60-
titleMatchStyle = titleMatchStyle.Foreground(t.Background()).Background(t.Primary()).Bold(true)
82+
titleStyle = t.S().TextSelected.Padding(0, 1).Width(c.width)
83+
titleMatchStyle = t.S().TextSelected.Underline(true)
6184
}
6285

6386
var truncatedTitle string

internal/tui/components/core/helpers.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,26 @@ import (
99
"github.com/opencode-ai/opencode/internal/tui/styles"
1010
)
1111

12-
func Section(title string, width int) string {
12+
func Section(text string, width int) string {
1313
t := styles.CurrentTheme()
1414
char := "─"
15+
length := len(text) + 1
16+
remainingWidth := width - length
17+
if remainingWidth > 0 {
18+
text = text + " " + t.S().Base.Foreground(t.Border).Render(strings.Repeat(char, remainingWidth))
19+
}
20+
return text
21+
}
22+
23+
func Title(title string, width int) string {
24+
t := styles.CurrentTheme()
25+
char := "╱"
1526
length := len(title) + 1
1627
remainingWidth := width - length
28+
lineStyle := t.S().Base.Foreground(t.Primary)
29+
titleStyle := t.S().Base.Foreground(t.Secondary)
1730
if remainingWidth > 0 {
18-
title = title + " " + t.S().Base.Foreground(t.Border).Render(strings.Repeat(char, remainingWidth))
31+
title = titleStyle.Render(title) + " " + lineStyle.Render(strings.Repeat(char, remainingWidth))
1932
}
2033
return title
2134
}

internal/tui/components/core/list/list.go

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type ListModel interface {
3939
ResetView() // Clear rendering cache and reset scroll position
4040
Items() []util.Model // Get all items in the list
4141
SelectedIndex() int // Get the index of the currently selected item
42+
SetSelected(int) tea.Cmd // Set the selected item by index and scroll to it
4243
Filter(string) tea.Cmd // Filter items based on a search term
4344
}
4445

@@ -133,11 +134,12 @@ type model struct {
133134
gapSize int // Number of empty lines between items
134135
padding []int // Padding around the list content
135136

136-
filterable bool // Whether items can be filtered
137-
filteredItems []util.Model // Filtered items based on current search
138-
input textinput.Model // Input field for filtering items
139-
hideFilterInput bool // Whether to hide the filter input field
140-
currentSearch string // Current search term for filtering
137+
filterable bool // Whether items can be filtered
138+
filterPlaceholder string // Placeholder text for filter input
139+
filteredItems []util.Model // Filtered items based on current search
140+
input textinput.Model // Input field for filtering items
141+
hideFilterInput bool // Whether to hide the filter input field
142+
currentSearch string // Current search term for filtering
141143
}
142144

143145
// listOptions is a function type for configuring list options.
@@ -195,29 +197,39 @@ func WithHideFilterInput(hide bool) listOptions {
195197
}
196198
}
197199

200+
// WithFilterPlaceholder sets the placeholder text for the filter input field.
201+
func WithFilterPlaceholder(placeholder string) listOptions {
202+
return func(m *model) {
203+
m.filterPlaceholder = placeholder
204+
}
205+
}
206+
198207
// New creates a new list model with the specified options.
199208
// The list starts with no items selected and requires SetItems to be called
200209
// or items to be provided via WithItems option.
201210
func New(opts ...listOptions) ListModel {
202211
m := &model{
203-
help: help.New(),
204-
keyMap: DefaultKeyMap(),
205-
allItems: []util.Model{},
206-
filteredItems: []util.Model{},
207-
renderState: newRenderState(),
208-
gapSize: DefaultGapSize,
209-
padding: []int{},
210-
selectionState: selectionState{selectedIndex: NoSelection},
212+
help: help.New(),
213+
keyMap: DefaultKeyMap(),
214+
allItems: []util.Model{},
215+
filteredItems: []util.Model{},
216+
renderState: newRenderState(),
217+
gapSize: DefaultGapSize,
218+
padding: []int{},
219+
selectionState: selectionState{selectedIndex: NoSelection},
220+
filterPlaceholder: "Type to filter...",
211221
}
212222
for _, opt := range opts {
213223
opt(m)
214224
}
215225

216226
if m.filterable && !m.hideFilterInput {
227+
t := styles.CurrentTheme()
217228
ti := textinput.New()
218-
ti.Placeholder = "Type to filter..."
229+
ti.Placeholder = m.filterPlaceholder
219230
ti.SetVirtualCursor(false)
220231
ti.Focus()
232+
ti.SetStyles(t.S().TextInput)
221233
m.input = ti
222234

223235
// disable j,k movements
@@ -616,7 +628,7 @@ func (m *model) isSectionHeader(index int) bool {
616628

617629
// findFirstSelectableItem finds the first item that is not a section header.
618630
func (m *model) findFirstSelectableItem() int {
619-
for i := 0; i < len(m.filteredItems); i++ {
631+
for i := range m.filteredItems {
620632
if !m.isSectionHeader(i) {
621633
return i
622634
}
@@ -944,7 +956,7 @@ func (m *model) SetSize(width int, height int) tea.Cmd {
944956
m.viewState.width = width
945957
m.ResetView()
946958
if m.filterable && !m.hideFilterInput {
947-
m.input.SetWidth(m.getItemWidth() - 3)
959+
m.input.SetWidth(m.getItemWidth() - 5)
948960
}
949961
return m.setAllItemsSize()
950962
}
@@ -1096,7 +1108,7 @@ func (m *model) SetItems(items []util.Model) tea.Cmd {
10961108
}
10971109

10981110
func (c *model) inputStyle() lipgloss.Style {
1099-
return styles.BaseStyle()
1111+
return styles.BaseStyle().Padding(0, 1, 1, 1)
11001112
}
11011113

11021114
// section represents a group of items under a section header.
@@ -1275,3 +1287,22 @@ func (m *model) SelectedIndex() int {
12751287
}
12761288
return m.selectionState.selectedIndex
12771289
}
1290+
1291+
// SetSelected sets the selected item by index and automatically scrolls to make it visible.
1292+
// If the index is invalid or points to a section header, it finds the nearest selectable item.
1293+
func (m *model) SetSelected(index int) tea.Cmd {
1294+
changeNeeded := m.selectionState.selectedIndex - index
1295+
cmds := []tea.Cmd{}
1296+
if changeNeeded < 0 {
1297+
for range -changeNeeded {
1298+
cmds = append(cmds, m.selectNextItem())
1299+
m.renderVisible()
1300+
}
1301+
} else if changeNeeded > 0 {
1302+
for range changeNeeded {
1303+
cmds = append(cmds, m.selectPreviousItem())
1304+
m.renderVisible()
1305+
}
1306+
}
1307+
return tea.Batch(cmds...)
1308+
}

internal/tui/components/dialogs/commands/arguments.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,9 @@ func (c *commandArgumentsDialogCmp) View() tea.View {
220220
}
221221

222222
func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
223-
offset := 13 + (1+c.focusIndex)*3
223+
row, col := c.Position()
224+
offset := row + 3 + (1+c.focusIndex)*3
224225
cursor.Y += offset
225-
_, col := c.Position()
226226
cursor.X = cursor.X + col + 3
227227
return cursor
228228
}
@@ -237,10 +237,11 @@ func (c *commandArgumentsDialogCmp) style() lipgloss.Style {
237237
BorderForeground(t.TextMuted())
238238
}
239239

240-
func (q *commandArgumentsDialogCmp) Position() (int, int) {
241-
row := 10
242-
col := q.wWidth / 2
243-
col -= q.width / 2
240+
func (c *commandArgumentsDialogCmp) Position() (int, int) {
241+
row := c.wHeight / 2
242+
row -= c.wHeight / 2
243+
col := c.wWidth / 2
244+
col -= c.width / 2
244245
return row, col
245246
}
246247

internal/tui/components/dialogs/commands/keys.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ type CommandsDialogKeyMap struct {
1414
func DefaultCommandsDialogKeyMap() CommandsDialogKeyMap {
1515
return CommandsDialogKeyMap{
1616
Select: key.NewBinding(
17-
key.WithKeys("enter"),
17+
key.WithKeys("enter", "tab", "ctrl+y"),
1818
key.WithHelp("enter", "confirm"),
1919
),
2020
Next: key.NewBinding(
21-
key.WithKeys("tab", "down"),
22-
key.WithHelp("tab/↓", "next"),
21+
key.WithKeys("down", "ctrl+n"),
22+
key.WithHelp("↓", "next item"),
2323
),
2424
Previous: key.NewBinding(
25-
key.WithKeys("shift+tab", "up"),
26-
key.WithHelp("shift+tab/↑", "previous"),
25+
key.WithKeys("up", "ctrl+p"),
26+
key.WithHelp("↑", "previous item"),
2727
),
2828
}
2929
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package sessions
2+
3+
import (
4+
"github.com/charmbracelet/bubbles/v2/key"
5+
"github.com/opencode-ai/opencode/internal/tui/layout"
6+
)
7+
8+
type KeyMap struct {
9+
Select key.Binding
10+
Next key.Binding
11+
Previous key.Binding
12+
}
13+
14+
func DefaultKeyMap() KeyMap {
15+
return KeyMap{
16+
Select: key.NewBinding(
17+
key.WithKeys("enter", "tab", "ctrl+y"),
18+
key.WithHelp("enter", "confirm"),
19+
),
20+
Next: key.NewBinding(
21+
key.WithKeys("down", "ctrl+n"),
22+
key.WithHelp("↓", "next item"),
23+
),
24+
Previous: key.NewBinding(
25+
key.WithKeys("up", "ctrl+p"),
26+
key.WithHelp("↑", "previous item"),
27+
),
28+
}
29+
}
30+
31+
// FullHelp implements help.KeyMap.
32+
func (k KeyMap) FullHelp() [][]key.Binding {
33+
m := [][]key.Binding{}
34+
slice := layout.KeyMapToSlice(k)
35+
for i := 0; i < len(slice); i += 4 {
36+
end := min(i+4, len(slice))
37+
m = append(m, slice[i:end])
38+
}
39+
return m
40+
}
41+
42+
// ShortHelp implements help.KeyMap.
43+
func (k KeyMap) ShortHelp() []key.Binding {
44+
return []key.Binding{
45+
key.NewBinding(
46+
47+
key.WithKeys("down", "up"),
48+
key.WithHelp("↑↓", "choose"),
49+
),
50+
k.Select,
51+
key.NewBinding(
52+
key.WithKeys("esc"),
53+
key.WithHelp("esc", "cancel"),
54+
),
55+
}
56+
}

0 commit comments

Comments
 (0)