mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 13:30:52 +00:00
wip: refactoring tui
This commit is contained in:
parent
ca0ea3f94d
commit
653965ef59
14 changed files with 502 additions and 1402 deletions
|
@ -119,6 +119,13 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.attachments = append(m.attachments, msg.Attachment)
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
if m.textarea.Value() != "" {
|
||||
m.textarea.Reset()
|
||||
return m, func() tea.Msg {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case "shift+enter":
|
||||
value := m.textarea.Value()
|
||||
m.textarea.SetValue(value + "\n")
|
||||
|
@ -264,8 +271,12 @@ func (m *editorComponent) View() string {
|
|||
)
|
||||
textarea = styles.BaseStyle().
|
||||
Width(m.width).
|
||||
Border(lipgloss.NormalBorder(), true, true).
|
||||
BorderForeground(t.Border()).
|
||||
PaddingTop(1).
|
||||
PaddingBottom(1).
|
||||
Background(t.BackgroundElement()).
|
||||
Border(lipgloss.ThickBorder(), false, true).
|
||||
BorderForeground(t.BorderActive()).
|
||||
BorderBackground(t.Background()).
|
||||
Render(textarea)
|
||||
|
||||
hint := base("enter") + muted(" send ") + base("shift") + muted("+") + base("enter") + muted(" newline")
|
||||
|
@ -287,6 +298,7 @@ func (m *editorComponent) View() string {
|
|||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
// m.attachmentsContent(),
|
||||
"",
|
||||
textarea,
|
||||
info,
|
||||
)
|
||||
|
@ -409,21 +421,21 @@ func (m *editorComponent) attachmentsContent() string {
|
|||
|
||||
func createTextArea(existing *textarea.Model) textarea.Model {
|
||||
t := theme.CurrentTheme()
|
||||
bgColor := t.Background()
|
||||
bgColor := t.BackgroundElement()
|
||||
textColor := t.Text()
|
||||
textMutedColor := t.TextMuted()
|
||||
|
||||
ta := textarea.New()
|
||||
ta.Placeholder = "It's prompting time..."
|
||||
|
||||
ta.Styles.Blurred.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Blurred.CursorLine = styles.BaseStyle().Background(bgColor)
|
||||
ta.Styles.Blurred.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.Styles.Blurred.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Focused.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Focused.CursorLine = styles.BaseStyle().Background(bgColor)
|
||||
ta.Styles.Focused.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.Styles.Focused.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Blurred.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Blurred.CursorLine = lipgloss.NewStyle().Background(bgColor)
|
||||
ta.Styles.Blurred.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.Styles.Blurred.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Focused.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Focused.CursorLine = lipgloss.NewStyle().Background(bgColor)
|
||||
ta.Styles.Focused.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.Styles.Focused.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Cursor.Color = t.Primary()
|
||||
|
||||
ta.Prompt = " "
|
||||
ta.ShowLineNumbers = false
|
||||
|
|
|
@ -292,8 +292,8 @@ func renderToolInvocation(
|
|||
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
||||
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
|
||||
body = ""
|
||||
filename := toolArgsMap["filePath"].(string)
|
||||
if metadata["preview"] != nil {
|
||||
if metadata["preview"] != nil && toolArgsMap["filePath"] != nil {
|
||||
filename := toolArgsMap["filePath"].(string)
|
||||
body = metadata["preview"].(string)
|
||||
body = renderFile(filename, body, WithTruncate(6))
|
||||
}
|
||||
|
|
|
@ -237,7 +237,7 @@ func (m *messagesComponent) renderView() {
|
|||
}
|
||||
|
||||
m.viewport.SetHeight(m.height - lipgloss.Height(m.header()))
|
||||
m.viewport.SetContent(strings.Join(centered, "\n"))
|
||||
m.viewport.SetContent("\n" + strings.Join(centered, "\n") + "\n")
|
||||
}
|
||||
|
||||
func (m *messagesComponent) header() string {
|
||||
|
|
|
@ -6,196 +6,115 @@ import (
|
|||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type helpComponent struct {
|
||||
width int
|
||||
height int
|
||||
keys []key.Binding
|
||||
type helpDialog struct {
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
bindings []key.Binding
|
||||
}
|
||||
|
||||
func (h *helpComponent) Init() tea.Cmd {
|
||||
// func (i bindingItem) Render(selected bool, width int) string {
|
||||
// t := theme.CurrentTheme()
|
||||
// baseStyle := styles.BaseStyle().
|
||||
// Width(width - 2).
|
||||
// Background(t.BackgroundElement())
|
||||
//
|
||||
// if selected {
|
||||
// baseStyle = baseStyle.
|
||||
// Background(t.Primary()).
|
||||
// Foreground(t.BackgroundElement()).
|
||||
// Bold(true)
|
||||
// } else {
|
||||
// baseStyle = baseStyle.
|
||||
// Foreground(t.Text())
|
||||
// }
|
||||
//
|
||||
// return baseStyle.Padding(0, 1).Render(i.binding.Help().Desc)
|
||||
// }
|
||||
|
||||
func (h *helpDialog) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *helpComponent) SetBindings(k []key.Binding) {
|
||||
h.keys = k
|
||||
}
|
||||
|
||||
func (h *helpComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
h.width = 90
|
||||
h.width = msg.Width
|
||||
h.height = msg.Height
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
|
||||
seen := make(map[string]struct{})
|
||||
result := make([]key.Binding, 0, len(bindings))
|
||||
// func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
|
||||
// seen := make(map[string]struct{})
|
||||
// result := make([]key.Binding, 0, len(bindings))
|
||||
//
|
||||
// // Process bindings in reverse order
|
||||
// for i := len(bindings) - 1; i >= 0; i-- {
|
||||
// b := bindings[i]
|
||||
// k := strings.Join(b.Keys(), " ")
|
||||
// if _, ok := seen[k]; ok {
|
||||
// // duplicate, skip
|
||||
// continue
|
||||
// }
|
||||
// seen[k] = struct{}{}
|
||||
// // Add to the beginning of result to maintain original order
|
||||
// result = append([]key.Binding{b}, result...)
|
||||
// }
|
||||
//
|
||||
// return result
|
||||
// }
|
||||
|
||||
// Process bindings in reverse order
|
||||
for i := len(bindings) - 1; i >= 0; i-- {
|
||||
b := bindings[i]
|
||||
k := strings.Join(b.Keys(), " ")
|
||||
if _, ok := seen[k]; ok {
|
||||
// duplicate, skip
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
// Add to the beginning of result to maintain original order
|
||||
result = append([]key.Binding{b}, result...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *helpComponent) render() string {
|
||||
func (h *helpDialog) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
helpKeyStyle := styles.Bold().
|
||||
Background(t.Background()).
|
||||
keyStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Text()).
|
||||
Padding(0, 1, 0, 0)
|
||||
|
||||
helpDescStyle := styles.Regular().
|
||||
Background(t.Background()).
|
||||
Bold(true)
|
||||
descStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.TextMuted())
|
||||
|
||||
// Compile list of bindings to render
|
||||
bindings := removeDuplicateBindings(h.keys)
|
||||
|
||||
// Enumerate through each group of bindings, populating a series of
|
||||
// pairs of columns, one for keys, one for descriptions
|
||||
var (
|
||||
pairs []string
|
||||
width int
|
||||
rows = 12 - 2
|
||||
)
|
||||
|
||||
for i := 0; i < len(bindings); i += rows {
|
||||
var (
|
||||
keys []string
|
||||
descs []string
|
||||
)
|
||||
for j := i; j < min(i+rows, len(bindings)); j++ {
|
||||
keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
|
||||
descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
|
||||
}
|
||||
|
||||
// Render pair of columns; beyond the first pair, render a three space
|
||||
// left margin, in order to visually separate the pairs.
|
||||
var cols []string
|
||||
if len(pairs) > 0 {
|
||||
cols = []string{baseStyle.Render(" ")}
|
||||
}
|
||||
|
||||
maxDescWidth := 0
|
||||
for _, desc := range descs {
|
||||
if maxDescWidth < lipgloss.Width(desc) {
|
||||
maxDescWidth = lipgloss.Width(desc)
|
||||
}
|
||||
}
|
||||
for i := range descs {
|
||||
remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
|
||||
if remainingWidth > 0 {
|
||||
descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
|
||||
}
|
||||
}
|
||||
maxKeyWidth := 0
|
||||
for _, key := range keys {
|
||||
if maxKeyWidth < lipgloss.Width(key) {
|
||||
maxKeyWidth = lipgloss.Width(key)
|
||||
}
|
||||
}
|
||||
for i := range keys {
|
||||
remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
|
||||
if remainingWidth > 0 {
|
||||
keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
|
||||
lines := []string{}
|
||||
for _, b := range h.bindings {
|
||||
content := keyStyle.Render(b.Help().Key)
|
||||
content += descStyle.Render(" " + b.Help().Desc)
|
||||
for i, key := range b.Keys() {
|
||||
if i == 0 {
|
||||
keyString := " (" + strings.ToUpper(key) + ")"
|
||||
// space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0)
|
||||
// spacer := strings.Repeat(" ", space)
|
||||
// content += descStyle.Render(spacer)
|
||||
content += descStyle.Render(keyString)
|
||||
}
|
||||
}
|
||||
|
||||
cols = append(cols,
|
||||
strings.Join(keys, "\n"),
|
||||
strings.Join(descs, "\n"),
|
||||
)
|
||||
|
||||
pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
|
||||
// check whether it exceeds the maximum width avail (the width of the
|
||||
// terminal, subtracting 2 for the borders).
|
||||
width += lipgloss.Width(pair)
|
||||
if width > h.width-2 {
|
||||
break
|
||||
}
|
||||
pairs = append(pairs, pair)
|
||||
lines = append(lines, content)
|
||||
}
|
||||
|
||||
// https://github.com/charmbracelet/lipgloss/v2/issues/209
|
||||
if len(pairs) > 1 {
|
||||
prefix := pairs[:len(pairs)-1]
|
||||
lastPair := pairs[len(pairs)-1]
|
||||
prefix = append(prefix, lipgloss.Place(
|
||||
lipgloss.Width(lastPair), // width
|
||||
lipgloss.Height(prefix[0]), // height
|
||||
lipgloss.Left, // x
|
||||
lipgloss.Top, // y
|
||||
lastPair, // content
|
||||
// lipgloss.WithWhitespaceBackground(t.Background()),
|
||||
))
|
||||
content := baseStyle.Width(h.width).Render(
|
||||
lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
prefix...,
|
||||
),
|
||||
)
|
||||
return content
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (h *helpDialog) Render(background string) string {
|
||||
return h.modal.Render(h.View(), background)
|
||||
}
|
||||
|
||||
func (h *helpDialog) Close() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
type HelpDialog interface {
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
func NewHelpDialog(bindings ...key.Binding) HelpDialog {
|
||||
return &helpDialog{
|
||||
bindings: bindings,
|
||||
modal: modal.New(),
|
||||
}
|
||||
|
||||
// Join pairs of columns and enclose in a border
|
||||
content := baseStyle.Width(h.width).Render(
|
||||
lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
pairs...,
|
||||
),
|
||||
)
|
||||
return content
|
||||
}
|
||||
|
||||
func (h *helpComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
content := h.render()
|
||||
header := baseStyle.
|
||||
Bold(true).
|
||||
Width(lipgloss.Width(content)).
|
||||
Foreground(t.Primary()).
|
||||
Render("Keyboard Shortcuts")
|
||||
|
||||
return baseStyle.Padding(1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(h.width).
|
||||
BorderBackground(t.Background()).
|
||||
Render(
|
||||
lipgloss.JoinVertical(lipgloss.Center,
|
||||
header,
|
||||
baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
|
||||
content,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type HelpComponent interface {
|
||||
layout.ModelWithView
|
||||
SetBindings([]key.Binding)
|
||||
}
|
||||
|
||||
func NewHelpCmp() HelpComponent {
|
||||
return &helpComponent{}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ import (
|
|||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/state"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
|
@ -19,25 +21,16 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
numVisibleModels = 10
|
||||
numVisibleModels = 6
|
||||
maxDialogWidth = 40
|
||||
)
|
||||
|
||||
// CloseModelDialogMsg is sent when a model is selected
|
||||
type CloseModelDialogMsg struct {
|
||||
Provider *client.ProviderInfo
|
||||
Model *client.ProviderModel
|
||||
}
|
||||
|
||||
// ModelDialog interface for the model selection dialog
|
||||
type ModelDialog interface {
|
||||
layout.ModelWithView
|
||||
layout.Bindings
|
||||
|
||||
SetProviders(providers []client.ProviderInfo)
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
type modelDialogComponent struct {
|
||||
type modelDialog struct {
|
||||
app *app.App
|
||||
availableProviders []client.ProviderInfo
|
||||
provider client.ProviderInfo
|
||||
|
@ -48,6 +41,8 @@ type modelDialogComponent struct {
|
|||
scrollOffset int
|
||||
hScrollOffset int
|
||||
hScrollPossible bool
|
||||
|
||||
modal *modal.Modal
|
||||
}
|
||||
|
||||
type modelKeyMap struct {
|
||||
|
@ -57,27 +52,23 @@ type modelKeyMap struct {
|
|||
Right key.Binding
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
J key.Binding
|
||||
K key.Binding
|
||||
H key.Binding
|
||||
L key.Binding
|
||||
}
|
||||
|
||||
var modelKeys = modelKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑", "previous model"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓", "next model"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left"),
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("←", "scroll left"),
|
||||
),
|
||||
Right: key.NewBinding(
|
||||
key.WithKeys("right"),
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("→", "scroll right"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
|
@ -88,25 +79,9 @@ var modelKeys = modelKeyMap{
|
|||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close"),
|
||||
),
|
||||
J: key.NewBinding(
|
||||
key.WithKeys("j"),
|
||||
key.WithHelp("j", "next model"),
|
||||
),
|
||||
K: key.NewBinding(
|
||||
key.WithKeys("k"),
|
||||
key.WithHelp("k", "previous model"),
|
||||
),
|
||||
H: key.NewBinding(
|
||||
key.WithKeys("h"),
|
||||
key.WithHelp("h", "scroll left"),
|
||||
),
|
||||
L: key.NewBinding(
|
||||
key.WithKeys("l"),
|
||||
key.WithHelp("l", "scroll right"),
|
||||
),
|
||||
}
|
||||
|
||||
func (m *modelDialogComponent) Init() tea.Cmd {
|
||||
func (m *modelDialog) Init() tea.Cmd {
|
||||
// cfg := config.Get()
|
||||
// modelInfo := GetSelectedModel(cfg)
|
||||
// m.availableProviders = getEnabledProviders(cfg)
|
||||
|
@ -116,40 +91,31 @@ func (m *modelDialogComponent) Init() tea.Cmd {
|
|||
// m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
|
||||
|
||||
// m.setupModelsForProvider(m.provider)
|
||||
|
||||
m.availableProviders, _ = m.app.ListProviders(context.Background())
|
||||
m.hScrollOffset = 0
|
||||
m.hScrollPossible = len(m.availableProviders) > 1
|
||||
m.provider = m.availableProviders[m.hScrollOffset]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *modelDialogComponent) SetProviders(providers []client.ProviderInfo) {
|
||||
m.availableProviders = providers
|
||||
}
|
||||
|
||||
func (m *modelDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, modelKeys.Up) || key.Matches(msg, modelKeys.K):
|
||||
case key.Matches(msg, modelKeys.Up):
|
||||
m.moveSelectionUp()
|
||||
case key.Matches(msg, modelKeys.Down) || key.Matches(msg, modelKeys.J):
|
||||
case key.Matches(msg, modelKeys.Down):
|
||||
m.moveSelectionDown()
|
||||
case key.Matches(msg, modelKeys.Left) || key.Matches(msg, modelKeys.H):
|
||||
case key.Matches(msg, modelKeys.Left):
|
||||
if m.hScrollPossible {
|
||||
m.switchProvider(-1)
|
||||
}
|
||||
case key.Matches(msg, modelKeys.Right) || key.Matches(msg, modelKeys.L):
|
||||
case key.Matches(msg, modelKeys.Right):
|
||||
if m.hScrollPossible {
|
||||
m.switchProvider(1)
|
||||
}
|
||||
case key.Matches(msg, modelKeys.Enter):
|
||||
models := m.models()
|
||||
return m, util.CmdHandler(CloseModelDialogMsg{Provider: &m.provider, Model: &models[m.selectedIdx]})
|
||||
cmd := util.CmdHandler(state.ModelSelectedMsg{Provider: m.provider, Model: models[m.selectedIdx]})
|
||||
return m, tea.Batch(cmd, util.CmdHandler(modal.CloseModalMsg{}))
|
||||
case key.Matches(msg, modelKeys.Escape):
|
||||
return m, util.CmdHandler(CloseModelDialogMsg{})
|
||||
return m, util.CmdHandler(modal.CloseModalMsg{})
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
|
@ -159,7 +125,7 @@ func (m *modelDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, nil
|
||||
}
|
||||
|
||||
func (m *modelDialogComponent) models() []client.ProviderModel {
|
||||
func (m *modelDialog) models() []client.ProviderModel {
|
||||
models := slices.SortedFunc(maps.Values(m.provider.Models), func(a, b client.ProviderModel) int {
|
||||
return strings.Compare(*a.Name, *b.Name)
|
||||
})
|
||||
|
@ -167,7 +133,7 @@ func (m *modelDialogComponent) models() []client.ProviderModel {
|
|||
}
|
||||
|
||||
// moveSelectionUp moves the selection up or wraps to bottom
|
||||
func (m *modelDialogComponent) moveSelectionUp() {
|
||||
func (m *modelDialog) moveSelectionUp() {
|
||||
if m.selectedIdx > 0 {
|
||||
m.selectedIdx--
|
||||
} else {
|
||||
|
@ -182,7 +148,7 @@ func (m *modelDialogComponent) moveSelectionUp() {
|
|||
}
|
||||
|
||||
// moveSelectionDown moves the selection down or wraps to top
|
||||
func (m *modelDialogComponent) moveSelectionDown() {
|
||||
func (m *modelDialog) moveSelectionDown() {
|
||||
if m.selectedIdx < len(m.provider.Models)-1 {
|
||||
m.selectedIdx++
|
||||
} else {
|
||||
|
@ -196,7 +162,7 @@ func (m *modelDialogComponent) moveSelectionDown() {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *modelDialogComponent) switchProvider(offset int) {
|
||||
func (m *modelDialog) switchProvider(offset int) {
|
||||
newOffset := m.hScrollOffset + offset
|
||||
|
||||
// Ensure we stay within bounds
|
||||
|
@ -212,9 +178,11 @@ func (m *modelDialogComponent) switchProvider(offset int) {
|
|||
m.setupModelsForProvider(m.provider.Id)
|
||||
}
|
||||
|
||||
func (m *modelDialogComponent) View() string {
|
||||
func (m *modelDialog) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Text())
|
||||
|
||||
// Capitalize first letter of provider name
|
||||
title := baseStyle.
|
||||
|
@ -232,8 +200,10 @@ func (m *modelDialogComponent) View() string {
|
|||
for i := m.scrollOffset; i < endIdx; i++ {
|
||||
itemStyle := baseStyle.Width(maxDialogWidth)
|
||||
if i == m.selectedIdx {
|
||||
itemStyle = itemStyle.Background(t.Primary()).
|
||||
Foreground(t.Background()).Bold(true)
|
||||
itemStyle = itemStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Bold(true)
|
||||
}
|
||||
modelItems = append(modelItems, itemStyle.Render(*models[i].Name))
|
||||
}
|
||||
|
@ -247,15 +217,10 @@ func (m *modelDialogComponent) View() string {
|
|||
scrollIndicator,
|
||||
)
|
||||
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
return content
|
||||
}
|
||||
|
||||
func (m *modelDialogComponent) getScrollIndicators(maxWidth int) string {
|
||||
func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
||||
var indicator string
|
||||
|
||||
if len(m.provider.Models) > numVisibleModels {
|
||||
|
@ -291,10 +256,6 @@ func (m *modelDialogComponent) getScrollIndicators(maxWidth int) string {
|
|||
Render(indicator)
|
||||
}
|
||||
|
||||
func (m *modelDialogComponent) BindingKeys() []key.Binding {
|
||||
return layout.KeyMapToSlice(modelKeys)
|
||||
}
|
||||
|
||||
// findProviderIndex returns the index of the provider in the list, or -1 if not found
|
||||
// func findProviderIndex(providers []string, provider string) int {
|
||||
// for i, p := range providers {
|
||||
|
@ -305,7 +266,7 @@ func (m *modelDialogComponent) BindingKeys() []key.Binding {
|
|||
// return -1
|
||||
// }
|
||||
|
||||
func (m *modelDialogComponent) setupModelsForProvider(_ string) {
|
||||
func (m *modelDialog) setupModelsForProvider(_ string) {
|
||||
m.selectedIdx = 0
|
||||
m.scrollOffset = 0
|
||||
|
||||
|
@ -331,8 +292,22 @@ func (m *modelDialogComponent) setupModelsForProvider(_ string) {
|
|||
// }
|
||||
}
|
||||
|
||||
func NewModelDialogCmp(app *app.App) ModelDialog {
|
||||
return &modelDialogComponent{
|
||||
app: app,
|
||||
func (m *modelDialog) Render(background string) string {
|
||||
return m.modal.Render(m.View(), background)
|
||||
}
|
||||
|
||||
func (s *modelDialog) Close() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewModelDialog(app *app.App) ModelDialog {
|
||||
availableProviders, _ := app.ListProviders(context.Background())
|
||||
|
||||
return &modelDialog{
|
||||
availableProviders: availableProviders,
|
||||
hScrollOffset: 0,
|
||||
hScrollPossible: len(availableProviders) > 1,
|
||||
provider: availableProviders[0],
|
||||
modal: modal.New(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
|
@ -14,14 +15,17 @@ import (
|
|||
|
||||
const question = "Are you sure you want to quit?"
|
||||
|
||||
type CloseQuitMsg struct{}
|
||||
|
||||
// QuitDialog interface for the quit confirmation dialog
|
||||
type QuitDialog interface {
|
||||
layout.ModelWithView
|
||||
layout.Bindings
|
||||
layout.Modal
|
||||
IsQuitDialog() bool
|
||||
}
|
||||
|
||||
type quitDialogComponent struct {
|
||||
type quitDialog struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
modal *modal.Modal
|
||||
selectedNo bool
|
||||
}
|
||||
|
||||
|
@ -30,12 +34,11 @@ type helpMapping struct {
|
|||
EnterSpace key.Binding
|
||||
Yes key.Binding
|
||||
No key.Binding
|
||||
Tab key.Binding
|
||||
}
|
||||
|
||||
var helpKeys = helpMapping{
|
||||
LeftRight: key.NewBinding(
|
||||
key.WithKeys("left", "right"),
|
||||
key.WithKeys("left", "right", "h", "l", "tab"),
|
||||
key.WithHelp("←/→", "switch options"),
|
||||
),
|
||||
EnterSpace: key.NewBinding(
|
||||
|
@ -43,58 +46,61 @@ var helpKeys = helpMapping{
|
|||
key.WithHelp("enter/space", "confirm"),
|
||||
),
|
||||
Yes: key.NewBinding(
|
||||
key.WithKeys("y", "Y"),
|
||||
key.WithKeys("y", "Y", "ctrl+c"),
|
||||
key.WithHelp("y/Y", "yes"),
|
||||
),
|
||||
No: key.NewBinding(
|
||||
key.WithKeys("n", "N"),
|
||||
key.WithHelp("n/N", "no"),
|
||||
),
|
||||
Tab: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("tab", "switch options"),
|
||||
),
|
||||
}
|
||||
|
||||
func (q *quitDialogComponent) Init() tea.Cmd {
|
||||
func (q *quitDialog) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *quitDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (q *quitDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
q.width = msg.Width
|
||||
q.height = msg.Height
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, helpKeys.LeftRight) || key.Matches(msg, helpKeys.Tab):
|
||||
case key.Matches(msg, helpKeys.LeftRight):
|
||||
q.selectedNo = !q.selectedNo
|
||||
return q, nil
|
||||
case key.Matches(msg, helpKeys.EnterSpace):
|
||||
if !q.selectedNo {
|
||||
return q, tea.Quit
|
||||
}
|
||||
return q, util.CmdHandler(CloseQuitMsg{})
|
||||
return q, tea.Batch(
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
)
|
||||
case key.Matches(msg, helpKeys.Yes):
|
||||
return q, tea.Quit
|
||||
case key.Matches(msg, helpKeys.No):
|
||||
return q, util.CmdHandler(CloseQuitMsg{})
|
||||
return q, tea.Batch(
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (q *quitDialogComponent) View() string {
|
||||
func (q *quitDialog) Render(background string) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
yesStyle := baseStyle
|
||||
noStyle := baseStyle
|
||||
spacerStyle := baseStyle.Background(t.Background())
|
||||
spacerStyle := baseStyle.Background(t.BackgroundElement())
|
||||
|
||||
if q.selectedNo {
|
||||
noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
|
||||
yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
noStyle = noStyle.Background(t.Primary()).Foreground(t.BackgroundElement())
|
||||
yesStyle = yesStyle.Background(t.BackgroundElement()).Foreground(t.Primary())
|
||||
} else {
|
||||
yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
|
||||
noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
yesStyle = yesStyle.Background(t.Primary()).Foreground(t.BackgroundElement())
|
||||
noStyle = noStyle.Background(t.BackgroundElement()).Foreground(t.Primary())
|
||||
}
|
||||
|
||||
yesButton := yesStyle.Padding(0, 1).Render("Yes")
|
||||
|
@ -117,20 +123,21 @@ func (q *quitDialogComponent) View() string {
|
|||
),
|
||||
)
|
||||
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
return q.modal.Render(content, background)
|
||||
}
|
||||
|
||||
func (q *quitDialogComponent) BindingKeys() []key.Binding {
|
||||
return layout.KeyMapToSlice(helpKeys)
|
||||
func (q *quitDialog) Close() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewQuitCmp() QuitDialog {
|
||||
return &quitDialogComponent{
|
||||
func (q *quitDialog) IsQuitDialog() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// NewQuitDialog creates a new quit confirmation dialog
|
||||
func NewQuitDialog() QuitDialog {
|
||||
return &quitDialog{
|
||||
selectedNo: true,
|
||||
modal: modal.New(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
"context"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
utilComponents "github.com/sst/opencode/internal/components/util"
|
||||
components "github.com/sst/opencode/internal/components/util"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/state"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
// CloseSessionDialogMsg is sent when the session dialog is closed
|
||||
type CloseSessionDialogMsg struct {
|
||||
Session *client.SessionInfo
|
||||
}
|
||||
|
||||
// SessionDialog interface for the session switching dialog
|
||||
type SessionDialog interface {
|
||||
tea.Model
|
||||
layout.Bindings
|
||||
SetSessions(sessions []client.SessionInfo)
|
||||
SetSelectedSession(sessionID string)
|
||||
Render(background string) string
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
type sessionItem struct {
|
||||
|
@ -49,163 +43,70 @@ func (s sessionItem) Render(selected bool, width int) string {
|
|||
return baseStyle.Padding(0, 1).Render(s.session.Title)
|
||||
}
|
||||
|
||||
// sessionDialogContent is the inner content of the session dialog
|
||||
type sessionDialogContent struct {
|
||||
sessions []client.SessionInfo
|
||||
type sessionDialog struct {
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
selectedSessionID string
|
||||
list utilComponents.SimpleList[sessionItem]
|
||||
list components.SimpleList[sessionItem]
|
||||
}
|
||||
|
||||
type sessionKeyMap struct {
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
}
|
||||
|
||||
var sessionKeys = sessionKeyMap{
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "select session"),
|
||||
),
|
||||
Escape: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close"),
|
||||
),
|
||||
}
|
||||
|
||||
func (s *sessionDialogContent) Init() tea.Cmd {
|
||||
func (s *sessionDialog) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sessionDialogContent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
s.width = msg.Width
|
||||
s.height = msg.Height
|
||||
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, sessionKeys.Enter):
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
|
||||
selectedSession := item.session
|
||||
s.selectedSessionID = selectedSession.Id
|
||||
|
||||
return s, util.CmdHandler(CloseSessionDialogMsg{
|
||||
Session: &selectedSession,
|
||||
})
|
||||
return s, tea.Batch(
|
||||
util.CmdHandler(state.SessionSelectedMsg(&selectedSession)),
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
)
|
||||
}
|
||||
case key.Matches(msg, sessionKeys.Escape):
|
||||
return s, util.CmdHandler(CloseSessionDialogMsg{})
|
||||
default:
|
||||
// Pass other key messages to the list component
|
||||
var cmd tea.Cmd
|
||||
listModel, cmd := s.list.Update(msg)
|
||||
s.list = listModel.(utilComponents.SimpleList[sessionItem])
|
||||
return s, cmd
|
||||
}
|
||||
}
|
||||
|
||||
// For non-key messages
|
||||
var cmd tea.Cmd
|
||||
listModel, cmd := s.list.Update(msg)
|
||||
s.list = listModel.(utilComponents.SimpleList[sessionItem])
|
||||
s.list = listModel.(components.SimpleList[sessionItem])
|
||||
return s, cmd
|
||||
}
|
||||
|
||||
func (s *sessionDialogContent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().Background(t.BackgroundElement())
|
||||
width := layout.Current.Container.Width - 12
|
||||
|
||||
if len(s.sessions) == 0 {
|
||||
return baseStyle.Padding(1, 2).
|
||||
Foreground(t.TextMuted()).
|
||||
Width(width).
|
||||
Render("No sessions available")
|
||||
}
|
||||
|
||||
// Set the max width for the list
|
||||
s.list.SetMaxWidth(width)
|
||||
|
||||
return s.list.View()
|
||||
func (s *sessionDialog) Render(background string) string {
|
||||
return s.modal.Render(s.list.View(), background)
|
||||
}
|
||||
|
||||
func (s *sessionDialogContent) BindingKeys() []key.Binding {
|
||||
// Combine session dialog keys with list keys
|
||||
dialogKeys := layout.KeyMapToSlice(sessionKeys)
|
||||
listKeys := s.list.BindingKeys()
|
||||
return append(dialogKeys, listKeys...)
|
||||
func (s *sessionDialog) Close() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sessionDialogComponent wraps the content with a modal
|
||||
type sessionDialogComponent struct {
|
||||
content *sessionDialogContent
|
||||
modal *modal.Modal
|
||||
}
|
||||
// NewSessionDialog creates a new session switching dialog
|
||||
func NewSessionDialog(app *app.App) SessionDialog {
|
||||
sessions, _ := app.ListSessions(context.Background())
|
||||
|
||||
func (s *sessionDialogComponent) Init() tea.Cmd {
|
||||
return s.modal.Init()
|
||||
}
|
||||
|
||||
func (s *sessionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m, cmd := s.modal.Update(msg)
|
||||
s.modal = m.(*modal.Modal)
|
||||
return s, cmd
|
||||
}
|
||||
|
||||
func (s *sessionDialogComponent) View() string {
|
||||
return s.modal.View()
|
||||
}
|
||||
|
||||
func (s *sessionDialogComponent) Render(background string) string {
|
||||
return s.modal.Render(background)
|
||||
}
|
||||
|
||||
func (s *sessionDialogComponent) BindingKeys() []key.Binding {
|
||||
return s.modal.BindingKeys()
|
||||
}
|
||||
|
||||
func (s *sessionDialogComponent) SetSessions(sessions []client.SessionInfo) {
|
||||
s.content.sessions = sessions
|
||||
|
||||
// Convert sessions to sessionItems
|
||||
var sessionItems []sessionItem
|
||||
|
||||
for _, sess := range sessions {
|
||||
sessionItems = append(sessionItems, sessionItem{session: sess})
|
||||
}
|
||||
|
||||
s.content.list.SetItems(sessionItems)
|
||||
}
|
||||
|
||||
func (s *sessionDialogComponent) SetSelectedSession(sessionID string) {
|
||||
s.content.selectedSessionID = sessionID
|
||||
|
||||
// Update the selected index if sessions are already loaded
|
||||
if len(s.content.sessions) > 0 {
|
||||
// Re-set the sessions to update the selection
|
||||
s.SetSessions(s.content.sessions)
|
||||
}
|
||||
}
|
||||
|
||||
// NewSessionDialogCmp creates a new session switching dialog
|
||||
func NewSessionDialogCmp() SessionDialog {
|
||||
list := utilComponents.NewSimpleList[sessionItem](
|
||||
[]sessionItem{},
|
||||
list := components.NewSimpleList(
|
||||
sessionItems,
|
||||
10, // maxVisibleSessions
|
||||
"No sessions available",
|
||||
true, // useAlphaNumericKeys
|
||||
)
|
||||
|
||||
content := &sessionDialogContent{
|
||||
sessions: []client.SessionInfo{},
|
||||
selectedSessionID: "",
|
||||
list: list,
|
||||
}
|
||||
|
||||
return &sessionDialogComponent{
|
||||
content: content,
|
||||
modal: modal.New(content, modal.WithTitle("Switch Session")),
|
||||
return &sessionDialog{
|
||||
list: list,
|
||||
modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
components "github.com/sst/opencode/internal/components/util"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/status"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
|
@ -16,184 +16,113 @@ type ThemeChangedMsg struct {
|
|||
ThemeName string
|
||||
}
|
||||
|
||||
// CloseThemeDialogMsg is sent when the theme dialog is closed
|
||||
type CloseThemeDialogMsg struct{}
|
||||
|
||||
// ThemeDialog interface for the theme switching dialog
|
||||
type ThemeDialog interface {
|
||||
layout.ModelWithView
|
||||
layout.Bindings
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
type themeDialogComponent struct {
|
||||
themes []string
|
||||
selectedIdx int
|
||||
width int
|
||||
height int
|
||||
currentTheme string
|
||||
type themeItem struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type themeKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
J key.Binding
|
||||
K key.Binding
|
||||
}
|
||||
func (t themeItem) Render(selected bool, width int) string {
|
||||
th := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().
|
||||
Width(width - 2).
|
||||
Background(th.BackgroundElement())
|
||||
|
||||
var themeKeys = themeKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "previous theme"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "next theme"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "select theme"),
|
||||
),
|
||||
Escape: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close"),
|
||||
),
|
||||
J: key.NewBinding(
|
||||
key.WithKeys("j"),
|
||||
key.WithHelp("j", "next theme"),
|
||||
),
|
||||
K: key.NewBinding(
|
||||
key.WithKeys("k"),
|
||||
key.WithHelp("k", "previous theme"),
|
||||
),
|
||||
}
|
||||
|
||||
func (t *themeDialogComponent) Init() tea.Cmd {
|
||||
// Load available themes and update selectedIdx based on current theme
|
||||
t.themes = theme.AvailableThemes()
|
||||
t.currentTheme = theme.CurrentThemeName()
|
||||
|
||||
// Find the current theme in the list
|
||||
for i, name := range t.themes {
|
||||
if name == t.currentTheme {
|
||||
t.selectedIdx = i
|
||||
break
|
||||
}
|
||||
if selected {
|
||||
baseStyle = baseStyle.
|
||||
Background(th.Primary()).
|
||||
Foreground(th.BackgroundElement()).
|
||||
Bold(true)
|
||||
} else {
|
||||
baseStyle = baseStyle.
|
||||
Foreground(th.Text())
|
||||
}
|
||||
|
||||
return baseStyle.Padding(0, 1).Render(t.name)
|
||||
}
|
||||
|
||||
type themeDialog struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
modal *modal.Modal
|
||||
list components.SimpleList[themeItem]
|
||||
}
|
||||
|
||||
func (t *themeDialog) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *themeDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
t.width = msg.Width
|
||||
t.height = msg.Height
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
|
||||
if t.selectedIdx > 0 {
|
||||
t.selectedIdx--
|
||||
}
|
||||
return t, nil
|
||||
case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J):
|
||||
if t.selectedIdx < len(t.themes)-1 {
|
||||
t.selectedIdx++
|
||||
}
|
||||
return t, nil
|
||||
case key.Matches(msg, themeKeys.Enter):
|
||||
if len(t.themes) > 0 {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
|
||||
previousTheme := theme.CurrentThemeName()
|
||||
selectedTheme := t.themes[t.selectedIdx]
|
||||
selectedTheme := item.name
|
||||
if previousTheme == selectedTheme {
|
||||
return t, util.CmdHandler(CloseThemeDialogMsg{})
|
||||
return t, util.CmdHandler(modal.CloseModalMsg{})
|
||||
}
|
||||
if err := theme.SetTheme(selectedTheme); err != nil {
|
||||
status.Error(err.Error())
|
||||
return t, nil
|
||||
}
|
||||
return t, util.CmdHandler(ThemeChangedMsg{
|
||||
ThemeName: selectedTheme,
|
||||
})
|
||||
return t, tea.Batch(
|
||||
util.CmdHandler(ThemeChangedMsg{ThemeName: selectedTheme}),
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
)
|
||||
}
|
||||
case key.Matches(msg, themeKeys.Escape):
|
||||
return t, util.CmdHandler(CloseThemeDialogMsg{})
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
t.width = msg.Width
|
||||
t.height = msg.Height
|
||||
}
|
||||
return t, nil
|
||||
|
||||
var cmd tea.Cmd
|
||||
listModel, cmd := t.list.Update(msg)
|
||||
t.list = listModel.(components.SimpleList[themeItem])
|
||||
return t, cmd
|
||||
}
|
||||
|
||||
func (t *themeDialogComponent) View() string {
|
||||
currentTheme := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
func (t *themeDialog) Render(background string) string {
|
||||
return t.modal.Render(t.list.View(), background)
|
||||
}
|
||||
|
||||
if len(t.themes) == 0 {
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(currentTheme.Background()).
|
||||
BorderForeground(currentTheme.TextMuted()).
|
||||
Width(40).
|
||||
Render("No themes available")
|
||||
}
|
||||
func (t *themeDialog) Close() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate max width needed for theme names
|
||||
maxWidth := 40 // Minimum width
|
||||
for _, themeName := range t.themes {
|
||||
if len(themeName) > maxWidth-4 { // Account for padding
|
||||
maxWidth = len(themeName) + 4
|
||||
// NewThemeDialog creates a new theme switching dialog
|
||||
func NewThemeDialog() ThemeDialog {
|
||||
themes := theme.AvailableThemes()
|
||||
currentTheme := theme.CurrentThemeName()
|
||||
|
||||
var themeItems []themeItem
|
||||
var selectedIdx int
|
||||
for i, name := range themes {
|
||||
themeItems = append(themeItems, themeItem{name: name})
|
||||
if name == currentTheme {
|
||||
selectedIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
|
||||
|
||||
// Build the theme list
|
||||
themeItems := make([]string, 0, len(t.themes))
|
||||
for i, themeName := range t.themes {
|
||||
itemStyle := baseStyle.Width(maxWidth)
|
||||
|
||||
if i == t.selectedIdx {
|
||||
itemStyle = itemStyle.
|
||||
Background(currentTheme.Primary()).
|
||||
Foreground(currentTheme.Background()).
|
||||
Bold(true)
|
||||
}
|
||||
|
||||
themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
|
||||
}
|
||||
|
||||
title := baseStyle.
|
||||
Foreground(currentTheme.Primary()).
|
||||
Bold(true).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Select Theme")
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
list := components.NewSimpleList(
|
||||
themeItems,
|
||||
10, // maxVisibleThemes
|
||||
"No themes available",
|
||||
true,
|
||||
)
|
||||
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(currentTheme.Background()).
|
||||
BorderForeground(currentTheme.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
// Set the initial selection to the current theme
|
||||
list.SetSelectedIndex(selectedIdx)
|
||||
|
||||
func (t *themeDialogComponent) BindingKeys() []key.Binding {
|
||||
return layout.KeyMapToSlice(themeKeys)
|
||||
}
|
||||
|
||||
// NewThemeDialogCmp creates a new theme switching dialog
|
||||
func NewThemeDialogCmp() ThemeDialog {
|
||||
return &themeDialogComponent{
|
||||
themes: []string{},
|
||||
selectedIdx: 0,
|
||||
currentTheme: "",
|
||||
return &themeDialog{
|
||||
list: list,
|
||||
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
package modal
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
// CloseModalMsg is a message to signal that the active modal should be closed.
|
||||
type CloseModalMsg struct{}
|
||||
|
||||
// Modal is a reusable modal component that handles frame rendering and overlay placement
|
||||
type Modal struct {
|
||||
content tea.Model
|
||||
width int
|
||||
height int
|
||||
title string
|
||||
showBorder bool
|
||||
borderStyle lipgloss.Border
|
||||
maxWidth int
|
||||
maxHeight int
|
||||
centerContent bool
|
||||
width int
|
||||
height int
|
||||
title string
|
||||
maxWidth int
|
||||
maxHeight int
|
||||
fitContent bool
|
||||
}
|
||||
|
||||
// ModalOption is a function that configures a Modal
|
||||
|
@ -32,24 +30,11 @@ func WithTitle(title string) ModalOption {
|
|||
}
|
||||
}
|
||||
|
||||
// WithBorder enables/disables the border
|
||||
func WithBorder(show bool) ModalOption {
|
||||
return func(m *Modal) {
|
||||
m.showBorder = show
|
||||
}
|
||||
}
|
||||
|
||||
// WithBorderStyle sets the border style
|
||||
func WithBorderStyle(style lipgloss.Border) ModalOption {
|
||||
return func(m *Modal) {
|
||||
m.borderStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxWidth sets the maximum width
|
||||
func WithMaxWidth(width int) ModalOption {
|
||||
return func(m *Modal) {
|
||||
m.maxWidth = width
|
||||
m.fitContent = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,22 +45,18 @@ func WithMaxHeight(height int) ModalOption {
|
|||
}
|
||||
}
|
||||
|
||||
// WithCenterContent centers the content within the modal
|
||||
func WithCenterContent(center bool) ModalOption {
|
||||
func WithFitContent(fit bool) ModalOption {
|
||||
return func(m *Modal) {
|
||||
m.centerContent = center
|
||||
m.fitContent = fit
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Modal with the given content and options
|
||||
func New(content tea.Model, opts ...ModalOption) *Modal {
|
||||
// New creates a new Modal with the given options
|
||||
func New(opts ...ModalOption) *Modal {
|
||||
m := &Modal{
|
||||
content: content,
|
||||
showBorder: true,
|
||||
borderStyle: lipgloss.ThickBorder(),
|
||||
maxWidth: 0,
|
||||
maxHeight: 0,
|
||||
centerContent: false,
|
||||
maxWidth: 0,
|
||||
maxHeight: 0,
|
||||
fitContent: true,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
|
@ -85,40 +66,24 @@ func New(content tea.Model, opts ...ModalOption) *Modal {
|
|||
return m
|
||||
}
|
||||
|
||||
func (m *Modal) Init() tea.Cmd {
|
||||
return m.content.Init()
|
||||
}
|
||||
|
||||
func (m *Modal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
}
|
||||
|
||||
// Pass all messages to the content
|
||||
var cmd tea.Cmd
|
||||
m.content, cmd = m.content.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Modal) View() string {
|
||||
// Render renders the modal centered on the screen
|
||||
func (m *Modal) Render(contentView string, background string) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
// Get the content view
|
||||
contentView := ""
|
||||
if v, ok := m.content.(layout.ModelWithView); ok {
|
||||
contentView = v.View()
|
||||
}
|
||||
|
||||
// Calculate dimensions
|
||||
outerWidth := layout.Current.Container.Width - 8
|
||||
if m.maxWidth > 0 && outerWidth > m.maxWidth {
|
||||
outerWidth = m.maxWidth
|
||||
}
|
||||
|
||||
|
||||
if m.fitContent {
|
||||
titleWidth := lipgloss.Width(m.title)
|
||||
contentWidth := lipgloss.Width(contentView)
|
||||
largestWidth := max(titleWidth+2, contentWidth)
|
||||
outerWidth = largestWidth + 6
|
||||
}
|
||||
|
||||
innerWidth := outerWidth - 4
|
||||
|
||||
|
||||
// Base style for the modal
|
||||
baseStyle := styles.BaseStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
|
@ -132,7 +97,7 @@ func (m *Modal) View() string {
|
|||
Bold(true).
|
||||
Width(innerWidth).
|
||||
Padding(0, 1)
|
||||
|
||||
|
||||
titleView := titleStyle.Render(m.title)
|
||||
finalContent = lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
|
@ -143,56 +108,36 @@ func (m *Modal) View() string {
|
|||
finalContent = contentView
|
||||
}
|
||||
|
||||
// Apply modal styling
|
||||
modalStyle := baseStyle.
|
||||
PaddingTop(1).
|
||||
PaddingBottom(1).
|
||||
PaddingLeft(2).
|
||||
PaddingRight(2)
|
||||
PaddingRight(2).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderLeftForeground(t.BackgroundSubtle()).
|
||||
BorderLeftBackground(t.Background()).
|
||||
BorderRightForeground(t.BackgroundSubtle()).
|
||||
BorderRightBackground(t.Background())
|
||||
|
||||
if m.showBorder {
|
||||
modalStyle = modalStyle.
|
||||
BorderStyle(m.borderStyle).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderLeftForeground(t.BackgroundSubtle()).
|
||||
BorderLeftBackground(t.Background()).
|
||||
BorderRightForeground(t.BackgroundSubtle()).
|
||||
BorderRightBackground(t.Background())
|
||||
}
|
||||
|
||||
return modalStyle.
|
||||
modalView := modalStyle.
|
||||
Width(outerWidth).
|
||||
Render(finalContent)
|
||||
}
|
||||
|
||||
// Render renders the modal centered on the screen
|
||||
func (m *Modal) Render(background string) string {
|
||||
modalView := m.View()
|
||||
|
||||
// Calculate position for centering
|
||||
bgHeight := lipgloss.Height(background)
|
||||
bgWidth := lipgloss.Width(background)
|
||||
modalHeight := lipgloss.Height(modalView)
|
||||
modalWidth := lipgloss.Width(modalView)
|
||||
|
||||
|
||||
row := (bgHeight - modalHeight) / 2
|
||||
col := (bgWidth - modalWidth) / 2
|
||||
|
||||
// Use PlaceOverlay to render the modal on top of the background
|
||||
|
||||
return layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
modalView,
|
||||
background,
|
||||
true, // shadow
|
||||
)
|
||||
}
|
||||
|
||||
// BindingKeys returns the key bindings from the content if it implements layout.Bindings
|
||||
func (m *Modal) BindingKeys() []key.Binding {
|
||||
if b, ok := m.content.(layout.Bindings); ok {
|
||||
return b.BindingKeys()
|
||||
}
|
||||
return []key.Binding{}
|
||||
}
|
|
@ -20,6 +20,7 @@ type SimpleList[T SimpleListItem] interface {
|
|||
GetSelectedItem() (item T, idx int)
|
||||
SetItems(items []T)
|
||||
GetItems() []T
|
||||
SetSelectedIndex(idx int)
|
||||
}
|
||||
|
||||
type simpleListComponent[T SimpleListItem] struct {
|
||||
|
@ -109,6 +110,12 @@ func (c *simpleListComponent[T]) SetMaxWidth(width int) {
|
|||
c.maxWidth = width
|
||||
}
|
||||
|
||||
func (c *simpleListComponent[T]) SetSelectedIndex(idx int) {
|
||||
if idx >= 0 && idx < len(c.items) {
|
||||
c.selectedIdx = idx
|
||||
}
|
||||
}
|
||||
|
||||
func (c *simpleListComponent[T]) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
|
|
@ -36,6 +36,12 @@ type LayoutInfo struct {
|
|||
Container Dimensions
|
||||
}
|
||||
|
||||
type Modal interface {
|
||||
tea.Model
|
||||
Render(background string) string
|
||||
Close() tea.Cmd
|
||||
}
|
||||
|
||||
type Focusable interface {
|
||||
Focus() tea.Cmd
|
||||
Blur() tea.Cmd
|
||||
|
|
|
@ -8,15 +8,9 @@ import (
|
|||
"github.com/muesli/ansi"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/muesli/termenv"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
|
||||
// Most of this code is borrowed from
|
||||
// https://github.com/charmbracelet/lipgloss/v2/pull/102
|
||||
// as well as the lipgloss library, with some modification for what I needed.
|
||||
|
||||
// Split a string into lines, additionally returning the size of the widest line.
|
||||
func getLines(s string) (lines []string, widest int) {
|
||||
lines = strings.Split(s, "\n")
|
||||
|
@ -33,42 +27,18 @@ func getLines(s string) (lines []string, widest int) {
|
|||
func PlaceOverlay(
|
||||
x, y int,
|
||||
fg, bg string,
|
||||
shadow bool, opts ...WhitespaceOption,
|
||||
opts ...WhitespaceOption,
|
||||
) string {
|
||||
fgLines, fgWidth := getLines(fg)
|
||||
bgLines, bgWidth := getLines(bg)
|
||||
bgHeight := len(bgLines)
|
||||
fgHeight := len(fgLines)
|
||||
|
||||
shadow = false
|
||||
|
||||
if shadow {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
var shadowbg string = ""
|
||||
shadowchar := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Background()).
|
||||
Render("░")
|
||||
bgchar := baseStyle.Render(" ")
|
||||
for i := 0; i <= fgHeight; i++ {
|
||||
if i == 0 {
|
||||
shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
|
||||
} else {
|
||||
shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
|
||||
fgLines, fgWidth = getLines(fg)
|
||||
fgHeight = len(fgLines)
|
||||
}
|
||||
|
||||
if fgWidth >= bgWidth && fgHeight >= bgHeight {
|
||||
// FIXME: return fg or bg?
|
||||
return fg
|
||||
}
|
||||
|
||||
// TODO: allow placement outside of the bg box?
|
||||
x = util.Clamp(x, 0, bgWidth-fgWidth)
|
||||
y = util.Clamp(y, 0, bgHeight-fgHeight)
|
||||
|
@ -122,13 +92,6 @@ func cutLeft(s string, cutWidth int) string {
|
|||
return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type whitespace struct {
|
||||
style termenv.Style
|
||||
chars string
|
||||
|
|
|
@ -12,10 +12,8 @@ import (
|
|||
"github.com/sst/opencode/internal/components/chat"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/state"
|
||||
"github.com/sst/opencode/internal/status"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
var ChatPage PageID = "chat"
|
||||
|
@ -30,17 +28,12 @@ type chatPage struct {
|
|||
}
|
||||
|
||||
type ChatKeyMap struct {
|
||||
NewSession key.Binding
|
||||
Cancel key.Binding
|
||||
ToggleTools key.Binding
|
||||
ShowCompletionDialog key.Binding
|
||||
}
|
||||
|
||||
var keyMap = ChatKeyMap{
|
||||
NewSession: key.NewBinding(
|
||||
key.WithKeys("ctrl+n"),
|
||||
key.WithHelp("ctrl+n", "new session"),
|
||||
),
|
||||
Cancel: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "cancel"),
|
||||
|
@ -101,17 +94,19 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
p.showCompletionDialog = false
|
||||
p.app.SetCompletionDialogOpen(false)
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
_, cmd := p.editor.Update(msg)
|
||||
if cmd != nil {
|
||||
return p, cmd
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, keyMap.ShowCompletionDialog):
|
||||
p.showCompletionDialog = true
|
||||
p.app.SetCompletionDialogOpen(true)
|
||||
// Continue sending keys to layout->chat
|
||||
case key.Matches(msg, keyMap.NewSession):
|
||||
p.app.Session = &client.SessionInfo{}
|
||||
p.app.Messages = []client.MessageInfo{}
|
||||
return p, tea.Batch(
|
||||
util.CmdHandler(state.SessionClearedMsg{}),
|
||||
)
|
||||
case key.Matches(msg, keyMap.Cancel):
|
||||
if p.app.Session.Id != "" {
|
||||
// Cancel the current session's generation process
|
||||
|
@ -173,7 +168,6 @@ func (p *chatPage) View() string {
|
|||
layoutHeight-editorHeight-lipgloss.Height(overlay),
|
||||
overlay,
|
||||
layoutView,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -208,7 +202,7 @@ func NewChatPage(app *app.App) layout.ModelWithView {
|
|||
layout.WithDirection(layout.FlexDirectionVertical),
|
||||
layout.WithPaneSizes(
|
||||
layout.FlexPaneSizeGrow,
|
||||
layout.FlexPaneSizeFixed(6),
|
||||
layout.FlexPaneSizeFixed(5),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package tui
|
|||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/cursor"
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
|
@ -14,6 +13,7 @@ import (
|
|||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/core"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/page"
|
||||
"github.com/sst/opencode/internal/state"
|
||||
|
@ -25,69 +25,41 @@ import (
|
|||
)
|
||||
|
||||
type keyMap struct {
|
||||
Quit key.Binding
|
||||
Help key.Binding
|
||||
NewSession key.Binding
|
||||
SwitchSession key.Binding
|
||||
Commands key.Binding
|
||||
Filepicker key.Binding
|
||||
Models key.Binding
|
||||
SwitchModel key.Binding
|
||||
SwitchTheme key.Binding
|
||||
Tools key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
const (
|
||||
quitKey = "q"
|
||||
)
|
||||
|
||||
var keys = keyMap{
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("ctrl+c"),
|
||||
key.WithHelp("ctrl+c", "quit"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("ctrl+_"),
|
||||
key.WithHelp("ctrl+?", "toggle help"),
|
||||
key.WithKeys("f1", "super+/", "super+h"),
|
||||
key.WithHelp("/help", "show help"),
|
||||
),
|
||||
NewSession: key.NewBinding(
|
||||
key.WithKeys("f2", "super+n"),
|
||||
key.WithHelp("/new", "new session"),
|
||||
),
|
||||
|
||||
SwitchSession: key.NewBinding(
|
||||
key.WithKeys("ctrl+s"),
|
||||
key.WithHelp("ctrl+s", "switch session"),
|
||||
key.WithKeys("f3", "super+s"),
|
||||
key.WithHelp("/sessions", "switch session"),
|
||||
),
|
||||
|
||||
Commands: key.NewBinding(
|
||||
key.WithKeys("ctrl+k"),
|
||||
key.WithHelp("ctrl+k", "commands"),
|
||||
SwitchModel: key.NewBinding(
|
||||
key.WithKeys("f4", "super+m"),
|
||||
key.WithHelp("/model", "switch model"),
|
||||
),
|
||||
Filepicker: key.NewBinding(
|
||||
key.WithKeys("ctrl+f"),
|
||||
key.WithHelp("ctrl+f", "select files to upload"),
|
||||
),
|
||||
Models: key.NewBinding(
|
||||
key.WithKeys("ctrl+o"),
|
||||
key.WithHelp("ctrl+o", "model selection"),
|
||||
),
|
||||
|
||||
SwitchTheme: key.NewBinding(
|
||||
key.WithKeys("ctrl+t"),
|
||||
key.WithHelp("ctrl+t", "switch theme"),
|
||||
key.WithKeys("f5", "super+t"),
|
||||
key.WithHelp("/theme", "switch theme"),
|
||||
),
|
||||
|
||||
Tools: key.NewBinding(
|
||||
key.WithKeys("f9"),
|
||||
key.WithHelp("f9", "show available tools"),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("f10", "ctrl+c", "super+q"),
|
||||
key.WithHelp("/quit", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
var helpEsc = key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "toggle help"),
|
||||
)
|
||||
|
||||
var returnKey = key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close"),
|
||||
)
|
||||
|
||||
type appModel struct {
|
||||
width, height int
|
||||
currentPage page.PageID
|
||||
|
@ -96,72 +68,22 @@ type appModel struct {
|
|||
loadedPages map[page.PageID]bool
|
||||
status core.StatusComponent
|
||||
app *app.App
|
||||
|
||||
showPermissions bool
|
||||
permissions dialog.PermissionDialogComponent
|
||||
|
||||
showHelp bool
|
||||
help dialog.HelpComponent
|
||||
|
||||
showQuit bool
|
||||
quit dialog.QuitDialog
|
||||
|
||||
showSessionDialog bool
|
||||
sessionDialog dialog.SessionDialog
|
||||
|
||||
showCommandDialog bool
|
||||
commandDialog dialog.CommandDialog
|
||||
commands []dialog.Command
|
||||
|
||||
showModelDialog bool
|
||||
modelDialog dialog.ModelDialog
|
||||
|
||||
showInitDialog bool
|
||||
initDialog dialog.InitDialogCmp
|
||||
|
||||
showFilepicker bool
|
||||
filepicker dialog.FilepickerComponent
|
||||
|
||||
showThemeDialog bool
|
||||
themeDialog dialog.ThemeDialog
|
||||
|
||||
showMultiArgumentsDialog bool
|
||||
multiArgumentsDialog dialog.MultiArgumentsDialogCmp
|
||||
|
||||
showToolsDialog bool
|
||||
toolsDialog dialog.ToolsDialog
|
||||
modal layout.Modal
|
||||
commands []dialog.Command
|
||||
}
|
||||
|
||||
func (a appModel) Init() tea.Cmd {
|
||||
t := theme.CurrentTheme()
|
||||
var cmds []tea.Cmd
|
||||
cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
|
||||
// cmds = append(cmds, tea.SetForegroundColor(t.Background()))
|
||||
cmds = append(cmds, tea.RequestBackgroundColor)
|
||||
|
||||
cmd := a.pages[a.currentPage].Init()
|
||||
a.loadedPages[a.currentPage] = true
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = a.status.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.quit.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.help.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.sessionDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.commandDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.modelDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.initDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.filepicker.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.themeDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.toolsDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// Check if we should show the init dialog
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
|
@ -192,6 +114,41 @@ func (a appModel) updateAllPages(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
|
||||
if a.modal != nil {
|
||||
isModalTrigger := false
|
||||
if _, ok := msg.(modal.CloseModalMsg); ok {
|
||||
a.modal = nil
|
||||
return a, nil
|
||||
}
|
||||
if msg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
a.modal = nil
|
||||
return a, nil
|
||||
case "ctrl+c":
|
||||
if _, ok := a.modal.(dialog.QuitDialog); !ok {
|
||||
quitDialog := dialog.NewQuitDialog()
|
||||
a.modal = quitDialog
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
|
||||
isModalTrigger = key.Matches(msg, keys.NewSession) ||
|
||||
key.Matches(msg, keys.SwitchSession) ||
|
||||
key.Matches(msg, keys.SwitchModel) ||
|
||||
key.Matches(msg, keys.SwitchTheme) ||
|
||||
key.Matches(msg, keys.Help) ||
|
||||
key.Matches(msg, keys.Quit)
|
||||
}
|
||||
|
||||
if !isModalTrigger {
|
||||
updatedModal, cmd := a.modal.Update(msg)
|
||||
a.modal = updatedModal.(layout.Modal)
|
||||
return a, cmd
|
||||
}
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.BackgroundColorMsg:
|
||||
|
@ -248,39 +205,24 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
},
|
||||
}
|
||||
|
||||
s, _ := a.status.Update(msg)
|
||||
s, cmd := a.status.Update(msg)
|
||||
a.status = s.(core.StatusComponent)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
updated, cmd := a.pages[a.currentPage].Update(msg)
|
||||
a.pages[a.currentPage] = updated.(layout.ModelWithView)
|
||||
cmds = append(cmds, cmd)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
prm, permCmd := a.permissions.Update(msg)
|
||||
a.permissions = prm.(dialog.PermissionDialogComponent)
|
||||
cmds = append(cmds, permCmd)
|
||||
|
||||
help, helpCmd := a.help.Update(msg)
|
||||
a.help = help.(dialog.HelpComponent)
|
||||
cmds = append(cmds, helpCmd)
|
||||
|
||||
session, sessionCmd := a.sessionDialog.Update(msg)
|
||||
a.sessionDialog = session.(dialog.SessionDialog)
|
||||
cmds = append(cmds, sessionCmd)
|
||||
|
||||
command, commandCmd := a.commandDialog.Update(msg)
|
||||
a.commandDialog = command.(dialog.CommandDialog)
|
||||
cmds = append(cmds, commandCmd)
|
||||
|
||||
filepicker, filepickerCmd := a.filepicker.Update(msg)
|
||||
a.filepicker = filepicker.(dialog.FilepickerComponent)
|
||||
cmds = append(cmds, filepickerCmd)
|
||||
|
||||
a.initDialog.SetSize(msg.Width, msg.Height)
|
||||
|
||||
if a.showMultiArgumentsDialog {
|
||||
a.multiArgumentsDialog.SetSize(msg.Width, msg.Height)
|
||||
args, argsCmd := a.multiArgumentsDialog.Update(msg)
|
||||
a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
|
||||
cmds = append(cmds, argsCmd, a.multiArgumentsDialog.Init())
|
||||
if a.modal != nil {
|
||||
s, cmd := a.modal.Update(msg)
|
||||
a.modal = s.(layout.Modal)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
return a, tea.Batch(cmds...)
|
||||
|
@ -300,36 +242,17 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
// case dialog.PermissionDeny:
|
||||
// a.app.Permissions.Deny(context.Background(), msg.Permission)
|
||||
// }
|
||||
a.showPermissions = false
|
||||
// a.showPermissions = false
|
||||
return a, nil
|
||||
|
||||
case page.PageChangeMsg:
|
||||
return a, a.moveToPage(msg.ID)
|
||||
|
||||
case dialog.CloseQuitMsg:
|
||||
a.showQuit = false
|
||||
return a, nil
|
||||
|
||||
case dialog.CloseSessionDialogMsg:
|
||||
a.showSessionDialog = false
|
||||
if msg.Session != nil {
|
||||
return a, util.CmdHandler(state.SessionSelectedMsg(msg.Session))
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case state.SessionSelectedMsg:
|
||||
a.app.Session = msg
|
||||
a.app.Messages, _ = a.app.ListMessages(context.Background(), msg.Id)
|
||||
return a.updateAllPages(msg)
|
||||
|
||||
case dialog.CloseModelDialogMsg:
|
||||
a.showModelDialog = false
|
||||
slog.Debug("closing model dialog", "msg", msg)
|
||||
if msg.Provider != nil && msg.Model != nil {
|
||||
return a, util.CmdHandler(state.ModelSelectedMsg{Provider: *msg.Provider, Model: *msg.Model})
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case state.ModelSelectedMsg:
|
||||
a.app.Provider = &msg.Provider
|
||||
a.app.Model = &msg.Model
|
||||
|
@ -338,388 +261,83 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
a.app.SaveConfig()
|
||||
return a.updateAllPages(msg)
|
||||
|
||||
case dialog.CloseCommandDialogMsg:
|
||||
a.showCommandDialog = false
|
||||
return a, nil
|
||||
|
||||
case dialog.CloseThemeDialogMsg:
|
||||
a.showThemeDialog = false
|
||||
return a, nil
|
||||
|
||||
case dialog.CloseToolsDialogMsg:
|
||||
a.showToolsDialog = false
|
||||
return a, nil
|
||||
|
||||
case dialog.ShowToolsDialogMsg:
|
||||
a.showToolsDialog = msg.Show
|
||||
return a, nil
|
||||
|
||||
case dialog.ThemeChangedMsg:
|
||||
a.app.Config.Theme = msg.ThemeName
|
||||
a.app.SaveConfig()
|
||||
|
||||
updated, cmd := a.pages[a.currentPage].Update(msg)
|
||||
a.pages[a.currentPage] = updated.(layout.ModelWithView)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
|
||||
// cmds = append(cmds, tea.RequestBackgroundColor)
|
||||
|
||||
a.pages[a.currentPage] = updated.(layout.ModelWithView)
|
||||
a.showThemeDialog = false
|
||||
status.Info("Theme changed to: " + msg.ThemeName)
|
||||
return a, tea.Batch(cmds...)
|
||||
|
||||
case dialog.ShowInitDialogMsg:
|
||||
a.showInitDialog = msg.Show
|
||||
return a, nil
|
||||
|
||||
case dialog.CloseInitDialogMsg:
|
||||
a.showInitDialog = false
|
||||
if msg.Initialize {
|
||||
return a, a.app.InitializeProject(context.Background())
|
||||
} else {
|
||||
// Mark the project as initialized without running the command
|
||||
if err := a.app.MarkProjectInitialized(context.Background()); err != nil {
|
||||
status.Error(err.Error())
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case dialog.CommandSelectedMsg:
|
||||
a.showCommandDialog = false
|
||||
// Execute the command handler if available
|
||||
if msg.Command.Handler != nil {
|
||||
return a, msg.Command.Handler(msg.Command)
|
||||
}
|
||||
status.Info("Command selected: " + msg.Command.Title)
|
||||
return a, nil
|
||||
|
||||
case dialog.ShowMultiArgumentsDialogMsg:
|
||||
// Show multi-arguments dialog
|
||||
a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames)
|
||||
a.showMultiArgumentsDialog = true
|
||||
return a, a.multiArgumentsDialog.Init()
|
||||
|
||||
case dialog.CloseMultiArgumentsDialogMsg:
|
||||
// Close multi-arguments dialog
|
||||
a.showMultiArgumentsDialog = false
|
||||
|
||||
// If submitted, replace all named arguments and run the command
|
||||
if msg.Submit {
|
||||
content := msg.Content
|
||||
|
||||
// Replace each named argument with its value
|
||||
for name, value := range msg.Args {
|
||||
placeholder := "$" + name
|
||||
content = strings.ReplaceAll(content, placeholder, value)
|
||||
}
|
||||
|
||||
// Execute the command with arguments
|
||||
return a, util.CmdHandler(dialog.CommandRunCustomMsg{
|
||||
Content: content,
|
||||
Args: msg.Args,
|
||||
})
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
// If multi-arguments dialog is open, let it handle the key press first
|
||||
if a.showMultiArgumentsDialog {
|
||||
args, cmd := a.multiArgumentsDialog.Update(msg)
|
||||
a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
|
||||
return a, cmd
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
updated, cmd := a.pages[a.currentPage].Update(msg)
|
||||
a.pages[a.currentPage] = updated.(layout.ModelWithView)
|
||||
if cmd != nil {
|
||||
return a, cmd
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, keys.Quit):
|
||||
a.showQuit = !a.showQuit
|
||||
if a.showHelp {
|
||||
a.showHelp = false
|
||||
}
|
||||
if a.showSessionDialog {
|
||||
a.showSessionDialog = false
|
||||
}
|
||||
if a.showCommandDialog {
|
||||
a.showCommandDialog = false
|
||||
}
|
||||
if a.showFilepicker {
|
||||
a.showFilepicker = false
|
||||
a.filepicker.ToggleFilepicker(a.showFilepicker)
|
||||
a.app.SetFilepickerOpen(a.showFilepicker)
|
||||
}
|
||||
if a.showModelDialog {
|
||||
a.showModelDialog = false
|
||||
}
|
||||
if a.showMultiArgumentsDialog {
|
||||
a.showMultiArgumentsDialog = false
|
||||
}
|
||||
if a.showToolsDialog {
|
||||
a.showToolsDialog = false
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.SwitchSession):
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
|
||||
// Close other dialogs
|
||||
a.showToolsDialog = false
|
||||
a.showThemeDialog = false
|
||||
a.showModelDialog = false
|
||||
a.showFilepicker = false
|
||||
|
||||
// Load sessions and show the dialog
|
||||
sessions, err := a.app.ListSessions(context.Background())
|
||||
if err != nil {
|
||||
status.Error(err.Error())
|
||||
return a, nil
|
||||
}
|
||||
if len(sessions) == 0 {
|
||||
status.Warn("No sessions available")
|
||||
return a, nil
|
||||
}
|
||||
a.sessionDialog.SetSessions(sessions)
|
||||
a.showSessionDialog = true
|
||||
return a, nil
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.Commands):
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog && !a.showFilepicker {
|
||||
// Close other dialogs
|
||||
a.showToolsDialog = false
|
||||
a.showModelDialog = false
|
||||
|
||||
// Show commands dialog
|
||||
if len(a.commands) == 0 {
|
||||
status.Warn("No commands available")
|
||||
return a, nil
|
||||
}
|
||||
a.commandDialog.SetCommands(a.commands)
|
||||
a.showCommandDialog = true
|
||||
return a, nil
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.Models):
|
||||
if a.showModelDialog {
|
||||
a.showModelDialog = false
|
||||
return a, nil
|
||||
}
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
|
||||
// Close other dialogs
|
||||
a.showToolsDialog = false
|
||||
a.showThemeDialog = false
|
||||
a.showFilepicker = false
|
||||
|
||||
// Load providers and show the dialog
|
||||
providers, err := a.app.ListProviders(context.Background())
|
||||
if err != nil {
|
||||
status.Error(err.Error())
|
||||
return a, nil
|
||||
}
|
||||
if len(providers) == 0 {
|
||||
status.Warn("No providers available")
|
||||
return a, nil
|
||||
}
|
||||
a.modelDialog.SetProviders(providers)
|
||||
|
||||
a.showModelDialog = true
|
||||
return a, nil
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.SwitchTheme):
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
|
||||
// Close other dialogs
|
||||
a.showToolsDialog = false
|
||||
a.showModelDialog = false
|
||||
a.showFilepicker = false
|
||||
|
||||
a.showThemeDialog = true
|
||||
return a, a.themeDialog.Init()
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.Tools):
|
||||
// Check if any other dialog is open
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions &&
|
||||
!a.showSessionDialog && !a.showCommandDialog && !a.showThemeDialog &&
|
||||
!a.showFilepicker && !a.showModelDialog && !a.showInitDialog &&
|
||||
!a.showMultiArgumentsDialog {
|
||||
// Toggle tools dialog
|
||||
a.showToolsDialog = !a.showToolsDialog
|
||||
if a.showToolsDialog {
|
||||
// Get tool names dynamically
|
||||
toolNames := getAvailableToolNames(a.app)
|
||||
a.toolsDialog.SetTools(toolNames)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, returnKey) || key.Matches(msg):
|
||||
if !a.filepicker.IsCWDFocused() {
|
||||
if a.showToolsDialog {
|
||||
a.showToolsDialog = false
|
||||
return a, nil
|
||||
}
|
||||
if a.showQuit {
|
||||
a.showQuit = !a.showQuit
|
||||
return a, nil
|
||||
}
|
||||
if a.showHelp {
|
||||
a.showHelp = !a.showHelp
|
||||
return a, nil
|
||||
}
|
||||
if a.showInitDialog {
|
||||
a.showInitDialog = false
|
||||
// TODO: should we not ask again?
|
||||
// Mark the project as initialized without running the command
|
||||
// if err := config.MarkProjectInitialized(); err != nil {
|
||||
// status.Error(err.Error())
|
||||
// return a, nil
|
||||
// }
|
||||
return a, nil
|
||||
}
|
||||
if a.showFilepicker {
|
||||
a.showFilepicker = false
|
||||
a.filepicker.ToggleFilepicker(a.showFilepicker)
|
||||
a.app.SetFilepickerOpen(a.showFilepicker)
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
case key.Matches(msg, keys.Help):
|
||||
if a.showQuit {
|
||||
return a, nil
|
||||
}
|
||||
a.showHelp = !a.showHelp
|
||||
|
||||
// Close other dialogs if opening help
|
||||
if a.showHelp {
|
||||
a.showToolsDialog = false
|
||||
}
|
||||
helpDialog := dialog.NewHelpDialog(
|
||||
keys.Help,
|
||||
keys.NewSession,
|
||||
keys.SwitchSession,
|
||||
keys.SwitchModel,
|
||||
keys.SwitchTheme,
|
||||
keys.Quit,
|
||||
)
|
||||
a.modal = helpDialog
|
||||
return a, nil
|
||||
case key.Matches(msg, helpEsc):
|
||||
if a.app.IsBusy() {
|
||||
if a.showQuit {
|
||||
return a, nil
|
||||
}
|
||||
a.showHelp = !a.showHelp
|
||||
return a, nil
|
||||
}
|
||||
case key.Matches(msg, keys.Filepicker):
|
||||
// Toggle filepicker
|
||||
a.showFilepicker = !a.showFilepicker
|
||||
a.filepicker.ToggleFilepicker(a.showFilepicker)
|
||||
a.app.SetFilepickerOpen(a.showFilepicker)
|
||||
// Close other dialogs if opening filepicker
|
||||
if a.showFilepicker {
|
||||
a.showToolsDialog = false
|
||||
a.showThemeDialog = false
|
||||
a.showModelDialog = false
|
||||
a.showCommandDialog = false
|
||||
a.showSessionDialog = false
|
||||
}
|
||||
|
||||
case key.Matches(msg, keys.NewSession):
|
||||
a.app.Session = &client.SessionInfo{}
|
||||
a.app.Messages = []client.MessageInfo{}
|
||||
return a, tea.Batch(
|
||||
util.CmdHandler(state.SessionClearedMsg{}),
|
||||
)
|
||||
|
||||
case key.Matches(msg, keys.SwitchModel):
|
||||
modelDialog := dialog.NewModelDialog(a.app)
|
||||
a.modal = modelDialog
|
||||
return a, nil
|
||||
|
||||
case key.Matches(msg, keys.SwitchSession):
|
||||
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||
a.modal = sessionDialog
|
||||
return a, nil
|
||||
|
||||
case key.Matches(msg, keys.SwitchTheme):
|
||||
themeDialog := dialog.NewThemeDialog()
|
||||
a.modal = themeDialog
|
||||
return a, nil
|
||||
|
||||
case key.Matches(msg, keys.Quit):
|
||||
quitDialog := dialog.NewQuitDialog()
|
||||
a.modal = quitDialog
|
||||
return a, nil
|
||||
}
|
||||
|
||||
default:
|
||||
f, filepickerCmd := a.filepicker.Update(msg)
|
||||
a.filepicker = f.(dialog.FilepickerComponent)
|
||||
cmds = append(cmds, filepickerCmd)
|
||||
}
|
||||
|
||||
if a.showFilepicker {
|
||||
f, filepickerCmd := a.filepicker.Update(msg)
|
||||
a.filepicker = f.(dialog.FilepickerComponent)
|
||||
cmds = append(cmds, filepickerCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showQuit {
|
||||
q, quitCmd := a.quit.Update(msg)
|
||||
a.quit = q.(dialog.QuitDialog)
|
||||
cmds = append(cmds, quitCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showPermissions {
|
||||
d, permissionsCmd := a.permissions.Update(msg)
|
||||
a.permissions = d.(dialog.PermissionDialogComponent)
|
||||
cmds = append(cmds, permissionsCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showSessionDialog {
|
||||
d, sessionCmd := a.sessionDialog.Update(msg)
|
||||
a.sessionDialog = d.(dialog.SessionDialog)
|
||||
cmds = append(cmds, sessionCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showCommandDialog {
|
||||
d, commandCmd := a.commandDialog.Update(msg)
|
||||
a.commandDialog = d.(dialog.CommandDialog)
|
||||
cmds = append(cmds, commandCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showModelDialog {
|
||||
d, modelCmd := a.modelDialog.Update(msg)
|
||||
a.modelDialog = d.(dialog.ModelDialog)
|
||||
cmds = append(cmds, modelCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showInitDialog {
|
||||
d, initCmd := a.initDialog.Update(msg)
|
||||
a.initDialog = d.(dialog.InitDialogCmp)
|
||||
cmds = append(cmds, initCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showThemeDialog {
|
||||
d, themeCmd := a.themeDialog.Update(msg)
|
||||
a.themeDialog = d.(dialog.ThemeDialog)
|
||||
cmds = append(cmds, themeCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showToolsDialog {
|
||||
d, toolsCmd := a.toolsDialog.Update(msg)
|
||||
a.toolsDialog = d.(dialog.ToolsDialog)
|
||||
cmds = append(cmds, toolsCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
// f, filepickerCmd := a.filepicker.Update(msg)
|
||||
// a.filepicker = f.(dialog.FilepickerComponent)
|
||||
// cmds = append(cmds, filepickerCmd)
|
||||
}
|
||||
|
||||
// update status bar
|
||||
s, cmd := a.status.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
a.status = s.(core.StatusComponent)
|
||||
|
||||
// update current page
|
||||
updated, cmd := a.pages[a.currentPage].Update(msg)
|
||||
a.pages[a.currentPage] = updated.(layout.ModelWithView)
|
||||
cmds = append(cmds, cmd)
|
||||
|
@ -731,12 +349,6 @@ func (a *appModel) RegisterCommand(cmd dialog.Command) {
|
|||
a.commands = append(a.commands, cmd)
|
||||
}
|
||||
|
||||
// getAvailableToolNames returns a list of all available tool names
|
||||
func getAvailableToolNames(_ *app.App) []string {
|
||||
// TODO: Tools not implemented in API yet
|
||||
return []string{"Tools not available in API mode"}
|
||||
}
|
||||
|
||||
func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
if _, ok := a.loadedPages[pageID]; !ok {
|
||||
|
@ -759,170 +371,10 @@ func (a appModel) View() string {
|
|||
a.pages[a.currentPage].View(),
|
||||
}
|
||||
components = append(components, a.status.View())
|
||||
|
||||
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
|
||||
|
||||
if a.showPermissions {
|
||||
overlay := a.permissions.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showFilepicker {
|
||||
overlay := a.filepicker.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
if a.showHelp {
|
||||
bindings := layout.KeyMapToSlice(keys)
|
||||
if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
|
||||
bindings = append(bindings, p.BindingKeys()...)
|
||||
}
|
||||
if a.showPermissions {
|
||||
bindings = append(bindings, a.permissions.BindingKeys()...)
|
||||
}
|
||||
if !a.app.IsBusy() {
|
||||
bindings = append(bindings, helpEsc)
|
||||
}
|
||||
a.help.SetBindings(bindings)
|
||||
|
||||
overlay := a.help.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showQuit {
|
||||
overlay := a.quit.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showSessionDialog {
|
||||
appView = a.sessionDialog.Render(appView)
|
||||
}
|
||||
|
||||
if a.showModelDialog {
|
||||
overlay := a.modelDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showCommandDialog {
|
||||
overlay := a.commandDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showInitDialog {
|
||||
overlay := a.initDialog.View()
|
||||
appView = layout.PlaceOverlay(
|
||||
a.width/2-lipgloss.Width(overlay)/2,
|
||||
a.height/2-lipgloss.Height(overlay)/2,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showThemeDialog {
|
||||
overlay := a.themeDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showMultiArgumentsDialog {
|
||||
overlay := a.multiArgumentsDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showToolsDialog {
|
||||
overlay := a.toolsDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
if a.modal != nil {
|
||||
appView = a.modal.Render(appView)
|
||||
}
|
||||
|
||||
return appView
|
||||
|
@ -931,24 +383,14 @@ func (a appModel) View() string {
|
|||
func NewModel(app *app.App) tea.Model {
|
||||
startPage := page.ChatPage
|
||||
model := &appModel{
|
||||
currentPage: startPage,
|
||||
loadedPages: make(map[page.PageID]bool),
|
||||
status: core.NewStatusCmp(app),
|
||||
help: dialog.NewHelpCmp(),
|
||||
quit: dialog.NewQuitCmp(),
|
||||
sessionDialog: dialog.NewSessionDialogCmp(),
|
||||
commandDialog: dialog.NewCommandDialogCmp(),
|
||||
modelDialog: dialog.NewModelDialogCmp(app),
|
||||
permissions: dialog.NewPermissionDialogCmp(),
|
||||
initDialog: dialog.NewInitDialogCmp(),
|
||||
themeDialog: dialog.NewThemeDialogCmp(),
|
||||
toolsDialog: dialog.NewToolsDialogCmp(),
|
||||
app: app,
|
||||
commands: []dialog.Command{},
|
||||
currentPage: startPage,
|
||||
loadedPages: make(map[page.PageID]bool),
|
||||
status: core.NewStatusCmp(app),
|
||||
app: app,
|
||||
commands: []dialog.Command{},
|
||||
pages: map[page.PageID]layout.ModelWithView{
|
||||
page.ChatPage: page.NewChatPage(app),
|
||||
},
|
||||
filepicker: dialog.NewFilepickerCmp(app),
|
||||
}
|
||||
|
||||
model.RegisterCommand(dialog.Command{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue