summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components/chat/editor.go
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-06-05 15:44:20 -0500
committeradamdottv <[email protected]>2025-06-11 11:43:28 -0500
commit95d5e1f2318e0c62f19196122fc2a448f1114cfd (patch)
tree75369872d32e10896e9263ddbf32cf36e7e418ac /packages/tui/internal/components/chat/editor.go
parent979bad3e64e3fff43d41094a79c73deb31e82ec8 (diff)
downloadopencode-95d5e1f2318e0c62f19196122fc2a448f1114cfd.tar.gz
opencode-95d5e1f2318e0c62f19196122fc2a448f1114cfd.zip
wip: refactoring tui
Diffstat (limited to 'packages/tui/internal/components/chat/editor.go')
-rw-r--r--packages/tui/internal/components/chat/editor.go254
1 files changed, 150 insertions, 104 deletions
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index 305365a25..52f198492 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -10,6 +10,7 @@ import (
"unicode"
"github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
@@ -23,7 +24,7 @@ import (
"github.com/sst/opencode/internal/util"
)
-type editorCmp struct {
+type editorComponent struct {
width int
height int
app *app.App
@@ -33,6 +34,7 @@ type editorCmp struct {
history []string
historyIndex int
currentMessage string
+ spinner spinner.Model
}
type EditorKeyMaps struct {
@@ -96,86 +98,19 @@ const (
maxAttachments = 5
)
-func (m *editorCmp) openEditor(value string) tea.Cmd {
- editor := os.Getenv("EDITOR")
- if editor == "" {
- editor = "nvim"
- }
-
- tmpfile, err := os.CreateTemp("", "msg_*.md")
- tmpfile.WriteString(value)
- if err != nil {
- status.Error(err.Error())
- return nil
- }
- tmpfile.Close()
- c := exec.Command(editor, tmpfile.Name()) //nolint:gosec
- c.Stdin = os.Stdin
- c.Stdout = os.Stdout
- c.Stderr = os.Stderr
- return tea.ExecProcess(c, func(err error) tea.Msg {
- if err != nil {
- status.Error(err.Error())
- return nil
- }
- content, err := os.ReadFile(tmpfile.Name())
- if err != nil {
- status.Error(err.Error())
- return nil
- }
- if len(content) == 0 {
- status.Warn("Message is empty")
- return nil
- }
- os.Remove(tmpfile.Name())
- attachments := m.attachments
- m.attachments = nil
- return SendMsg{
- Text: string(content),
- Attachments: attachments,
- }
- })
-}
-
-func (m *editorCmp) Init() tea.Cmd {
- return textarea.Blink
-}
-
-func (m *editorCmp) send() tea.Cmd {
- value := m.textarea.Value()
- m.textarea.Reset()
- attachments := m.attachments
-
- // Save to history if not empty and not a duplicate of the last entry
- if value != "" {
- if len(m.history) == 0 || m.history[len(m.history)-1] != value {
- m.history = append(m.history, value)
- }
- m.historyIndex = len(m.history)
- m.currentMessage = ""
- }
-
- m.attachments = nil
- if value == "" {
- return nil
- }
- return tea.Batch(
- util.CmdHandler(SendMsg{
- Text: value,
- Attachments: attachments,
- }),
- )
+func (m *editorComponent) Init() tea.Cmd {
+ return tea.Batch(textarea.Blink, m.spinner.Tick)
}
-func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
var cmd tea.Cmd
switch msg := msg.(type) {
case dialog.ThemeChangedMsg:
- m.textarea = CreateTextArea(&m.textarea)
+ m.textarea = createTextArea(&m.textarea)
case dialog.CompletionSelectedMsg:
existingValue := m.textarea.Value()
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
-
m.textarea.SetValue(modifiedValue)
return m, nil
case dialog.AttachmentAddedMsg:
@@ -296,47 +231,160 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.send()
}
}
-
}
+
+ m.spinner, cmd = m.spinner.Update(msg)
+ cmds = append(cmds, cmd)
+
m.textarea, cmd = m.textarea.Update(msg)
- return m, cmd
+ cmds = append(cmds, cmd)
+
+ return m, tea.Batch(cmds...)
}
-func (m *editorCmp) View() string {
+func (m *editorComponent) View() string {
t := theme.CurrentTheme()
-
- // Style the prompt with theme colors
- style := lipgloss.NewStyle().
+ base := styles.BaseStyle().Render
+ muted := styles.Muted().Render
+ promptStyle := lipgloss.NewStyle().
Padding(0, 0, 0, 1).
Bold(true).
Foreground(t.Primary())
+ prompt := promptStyle.Render(">")
- if len(m.attachments) == 0 {
- return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
+ textarea := lipgloss.JoinHorizontal(
+ lipgloss.Top,
+ prompt,
+ m.textarea.View(),
+ )
+ textarea = styles.BaseStyle().
+ Width(m.width-2).
+ Border(lipgloss.NormalBorder(), true, true).
+ BorderForeground(t.Border()).
+ Render(textarea)
+
+ hint := base("enter") + muted(" send ") + base("shift") + muted("+") + base("enter") + muted(" newline")
+ if m.app.IsBusy() {
+ hint = muted("working") + m.spinner.View() + muted(" ") + base("esc") + muted(" interrupt")
+ }
+
+ model := ""
+ if m.app.Model != nil {
+ model = base(*m.app.Model.Name) + muted(" • /model")
}
- m.textarea.SetHeight(m.height - 1)
- return lipgloss.JoinVertical(lipgloss.Top,
- m.attachmentsContent(),
- lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"),
- m.textarea.View()),
+
+ space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
+ spacer := lipgloss.NewStyle().Width(space).Render("")
+
+ info := lipgloss.JoinHorizontal(lipgloss.Left, hint, spacer, model)
+ info = styles.Padded().Render(info)
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Top,
+ // m.attachmentsContent(),
+ textarea,
+ info,
+ )
+
+ return styles.ForceReplaceBackgroundWithLipgloss(
+ content,
+ t.Background(),
)
}
-func (m *editorCmp) SetSize(width, height int) tea.Cmd {
+func (m *editorComponent) SetSize(width, height int) tea.Cmd {
m.width = width
m.height = height
- m.textarea.SetWidth(width - 3) // account for the prompt and padding right
- m.textarea.SetHeight(height)
+ m.textarea.SetWidth(width - 5) // account for the prompt and padding right
+ m.textarea.SetHeight(height - 3) // account for info underneath
return nil
}
-func (m *editorCmp) GetSize() (int, int) {
- return m.textarea.Width(), m.textarea.Height()
+func (m *editorComponent) GetSize() (int, int) {
+ return m.width, m.height
}
-func (m *editorCmp) attachmentsContent() string {
- var styledAttachments []string
+func (m *editorComponent) BindingKeys() []key.Binding {
+ bindings := []key.Binding{}
+ bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...)
+ bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...)
+ return bindings
+}
+
+func (m *editorComponent) openEditor(value string) tea.Cmd {
+ editor := os.Getenv("EDITOR")
+ if editor == "" {
+ editor = "nvim"
+ }
+
+ tmpfile, err := os.CreateTemp("", "msg_*.md")
+ tmpfile.WriteString(value)
+ if err != nil {
+ status.Error(err.Error())
+ return nil
+ }
+ tmpfile.Close()
+ c := exec.Command(editor, tmpfile.Name()) //nolint:gosec
+ c.Stdin = os.Stdin
+ c.Stdout = os.Stdout
+ c.Stderr = os.Stderr
+ return tea.ExecProcess(c, func(err error) tea.Msg {
+ if err != nil {
+ status.Error(err.Error())
+ return nil
+ }
+ content, err := os.ReadFile(tmpfile.Name())
+ if err != nil {
+ status.Error(err.Error())
+ return nil
+ }
+ if len(content) == 0 {
+ status.Warn("Message is empty")
+ return nil
+ }
+ os.Remove(tmpfile.Name())
+ attachments := m.attachments
+ m.attachments = nil
+ return SendMsg{
+ Text: string(content),
+ Attachments: attachments,
+ }
+ })
+}
+
+func (m *editorComponent) send() tea.Cmd {
+ value := m.textarea.Value()
+ m.textarea.Reset()
+ attachments := m.attachments
+
+ // Save to history if not empty and not a duplicate of the last entry
+ if value != "" {
+ if len(m.history) == 0 || m.history[len(m.history)-1] != value {
+ m.history = append(m.history, value)
+ }
+ m.historyIndex = len(m.history)
+ m.currentMessage = ""
+ }
+
+ m.attachments = nil
+ if value == "" {
+ return nil
+ }
+ return tea.Batch(
+ util.CmdHandler(SendMsg{
+ Text: value,
+ Attachments: attachments,
+ }),
+ )
+}
+
+func (m *editorComponent) attachmentsContent() string {
+ if len(m.attachments) == 0 {
+ return ""
+ }
+
t := theme.CurrentTheme()
+ var styledAttachments []string
attachmentStyles := styles.BaseStyle().
MarginLeft(1).
Background(t.TextMuted()).
@@ -357,20 +405,15 @@ func (m *editorCmp) attachmentsContent() string {
return content
}
-func (m *editorCmp) BindingKeys() []key.Binding {
- bindings := []key.Binding{}
- bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...)
- bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...)
- return bindings
-}
-
-func CreateTextArea(existing *textarea.Model) textarea.Model {
+func createTextArea(existing *textarea.Model) textarea.Model {
t := theme.CurrentTheme()
bgColor := t.Background()
textColor := t.Text()
textMutedColor := t.TextMuted()
ta := textarea.New()
+ ta.Placeholder = "It's prompting time..."
+
ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor)
ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
@@ -394,13 +437,16 @@ func CreateTextArea(existing *textarea.Model) textarea.Model {
return ta
}
-func NewEditorCmp(app *app.App) tea.Model {
- ta := CreateTextArea(nil)
- return &editorCmp{
+func NewEditorComponent(app *app.App) tea.Model {
+ s := spinner.New(spinner.WithSpinner(spinner.Ellipsis), spinner.WithStyle(styles.Muted().Width(3)))
+ ta := createTextArea(nil)
+
+ return &editorComponent{
app: app,
textarea: ta,
history: []string{},
historyIndex: 0,
currentMessage: "",
+ spinner: s,
}
}