summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/components
diff options
context:
space:
mode:
authorKujtim Hoxha <[email protected]>2025-03-25 13:04:36 +0100
committerKujtim Hoxha <[email protected]>2025-03-26 01:12:30 +0100
commit904061c243f70696bfe781e97bf4e392e6954d07 (patch)
tree4428f96d09968ee0cde44e6ebbaee4757f80050e /internal/tui/components
parent005b8ac16776512b2d4b1f22bd989da162ca1bad (diff)
downloadopencode-904061c243f70696bfe781e97bf4e392e6954d07.tar.gz
opencode-904061c243f70696bfe781e97bf4e392e6954d07.zip
additional tools
Diffstat (limited to 'internal/tui/components')
-rw-r--r--internal/tui/components/core/button.go287
-rw-r--r--internal/tui/components/dialog/permission.go167
-rw-r--r--internal/tui/components/messages/message.go108
-rw-r--r--internal/tui/components/repl/editor.go14
-rw-r--r--internal/tui/components/repl/messages.go180
-rw-r--r--internal/tui/components/repl/sessions.go8
6 files changed, 737 insertions, 27 deletions
diff --git a/internal/tui/components/core/button.go b/internal/tui/components/core/button.go
new file mode 100644
index 000000000..102957e0e
--- /dev/null
+++ b/internal/tui/components/core/button.go
@@ -0,0 +1,287 @@
+package core
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
+)
+
+// ButtonKeyMap defines key bindings for the button component
+type ButtonKeyMap struct {
+ Enter key.Binding
+}
+
+// DefaultButtonKeyMap returns default key bindings for the button
+func DefaultButtonKeyMap() ButtonKeyMap {
+ return ButtonKeyMap{
+ Enter: key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "select"),
+ ),
+ }
+}
+
+// ShortHelp returns keybinding help
+func (k ButtonKeyMap) ShortHelp() []key.Binding {
+ return []key.Binding{k.Enter}
+}
+
+// FullHelp returns full help info for keybindings
+func (k ButtonKeyMap) FullHelp() [][]key.Binding {
+ return [][]key.Binding{
+ {k.Enter},
+ }
+}
+
+// ButtonState represents the state of a button
+type ButtonState int
+
+const (
+ // ButtonNormal is the default state
+ ButtonNormal ButtonState = iota
+ // ButtonHovered is when the button is focused/hovered
+ ButtonHovered
+ // ButtonPressed is when the button is being pressed
+ ButtonPressed
+ // ButtonDisabled is when the button is disabled
+ ButtonDisabled
+)
+
+// ButtonVariant defines the visual style variant of a button
+type ButtonVariant int
+
+const (
+ // ButtonPrimary uses primary color styling
+ ButtonPrimary ButtonVariant = iota
+ // ButtonSecondary uses secondary color styling
+ ButtonSecondary
+ // ButtonDanger uses danger/error color styling
+ ButtonDanger
+ // ButtonWarning uses warning color styling
+ ButtonWarning
+ // ButtonNeutral uses neutral color styling
+ ButtonNeutral
+)
+
+// ButtonMsg is sent when a button is clicked
+type ButtonMsg struct {
+ ID string
+ Payload interface{}
+}
+
+// ButtonCmp represents a clickable button component
+type ButtonCmp struct {
+ id string
+ label string
+ width int
+ height int
+ state ButtonState
+ variant ButtonVariant
+ keyMap ButtonKeyMap
+ payload interface{}
+ style lipgloss.Style
+ hoverStyle lipgloss.Style
+}
+
+// NewButtonCmp creates a new button component
+func NewButtonCmp(id, label string) *ButtonCmp {
+ b := &ButtonCmp{
+ id: id,
+ label: label,
+ state: ButtonNormal,
+ variant: ButtonPrimary,
+ keyMap: DefaultButtonKeyMap(),
+ width: len(label) + 4, // add some padding
+ height: 1,
+ }
+ b.updateStyles()
+ return b
+}
+
+// WithVariant sets the button variant
+func (b *ButtonCmp) WithVariant(variant ButtonVariant) *ButtonCmp {
+ b.variant = variant
+ b.updateStyles()
+ return b
+}
+
+// WithPayload sets the payload sent with button events
+func (b *ButtonCmp) WithPayload(payload interface{}) *ButtonCmp {
+ b.payload = payload
+ return b
+}
+
+// WithWidth sets a custom width
+func (b *ButtonCmp) WithWidth(width int) *ButtonCmp {
+ b.width = width
+ b.updateStyles()
+ return b
+}
+
+// updateStyles recalculates styles based on current state and variant
+func (b *ButtonCmp) updateStyles() {
+ // Base styles
+ b.style = styles.Regular.
+ Padding(0, 1).
+ Width(b.width).
+ Align(lipgloss.Center).
+ BorderStyle(lipgloss.RoundedBorder())
+
+ b.hoverStyle = b.style.
+ Bold(true)
+
+ // Variant-specific styling
+ switch b.variant {
+ case ButtonPrimary:
+ b.style = b.style.
+ Foreground(styles.Base).
+ Background(styles.Primary).
+ BorderForeground(styles.Primary)
+
+ b.hoverStyle = b.hoverStyle.
+ Foreground(styles.Base).
+ Background(styles.Blue).
+ BorderForeground(styles.Blue)
+
+ case ButtonSecondary:
+ b.style = b.style.
+ Foreground(styles.Base).
+ Background(styles.Secondary).
+ BorderForeground(styles.Secondary)
+
+ b.hoverStyle = b.hoverStyle.
+ Foreground(styles.Base).
+ Background(styles.Mauve).
+ BorderForeground(styles.Mauve)
+
+ case ButtonDanger:
+ b.style = b.style.
+ Foreground(styles.Base).
+ Background(styles.Error).
+ BorderForeground(styles.Error)
+
+ b.hoverStyle = b.hoverStyle.
+ Foreground(styles.Base).
+ Background(styles.Red).
+ BorderForeground(styles.Red)
+
+ case ButtonWarning:
+ b.style = b.style.
+ Foreground(styles.Text).
+ Background(styles.Warning).
+ BorderForeground(styles.Warning)
+
+ b.hoverStyle = b.hoverStyle.
+ Foreground(styles.Text).
+ Background(styles.Peach).
+ BorderForeground(styles.Peach)
+
+ case ButtonNeutral:
+ b.style = b.style.
+ Foreground(styles.Text).
+ Background(styles.Grey).
+ BorderForeground(styles.Grey)
+
+ b.hoverStyle = b.hoverStyle.
+ Foreground(styles.Text).
+ Background(styles.DarkGrey).
+ BorderForeground(styles.DarkGrey)
+ }
+
+ // Disabled style override
+ if b.state == ButtonDisabled {
+ b.style = b.style.
+ Foreground(styles.SubText0).
+ Background(styles.LightGrey).
+ BorderForeground(styles.LightGrey)
+ }
+}
+
+// SetSize sets the button size
+func (b *ButtonCmp) SetSize(width, height int) {
+ b.width = width
+ b.height = height
+ b.updateStyles()
+}
+
+// Focus sets the button to focused state
+func (b *ButtonCmp) Focus() tea.Cmd {
+ if b.state != ButtonDisabled {
+ b.state = ButtonHovered
+ }
+ return nil
+}
+
+// Blur sets the button to normal state
+func (b *ButtonCmp) Blur() tea.Cmd {
+ if b.state != ButtonDisabled {
+ b.state = ButtonNormal
+ }
+ return nil
+}
+
+// Disable sets the button to disabled state
+func (b *ButtonCmp) Disable() {
+ b.state = ButtonDisabled
+ b.updateStyles()
+}
+
+// Enable enables the button if disabled
+func (b *ButtonCmp) Enable() {
+ if b.state == ButtonDisabled {
+ b.state = ButtonNormal
+ b.updateStyles()
+ }
+}
+
+// IsDisabled returns whether the button is disabled
+func (b *ButtonCmp) IsDisabled() bool {
+ return b.state == ButtonDisabled
+}
+
+// IsFocused returns whether the button is focused
+func (b *ButtonCmp) IsFocused() bool {
+ return b.state == ButtonHovered
+}
+
+// Init initializes the button
+func (b *ButtonCmp) Init() tea.Cmd {
+ return nil
+}
+
+// Update handles messages and user input
+func (b *ButtonCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ // Skip updates if disabled
+ if b.state == ButtonDisabled {
+ return b, nil
+ }
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ // Handle key presses when focused
+ if b.state == ButtonHovered {
+ switch {
+ case key.Matches(msg, b.keyMap.Enter):
+ b.state = ButtonPressed
+ return b, func() tea.Msg {
+ return ButtonMsg{
+ ID: b.id,
+ Payload: b.payload,
+ }
+ }
+ }
+ }
+ }
+
+ return b, nil
+}
+
+// View renders the button
+func (b *ButtonCmp) View() string {
+ if b.state == ButtonHovered || b.state == ButtonPressed {
+ return b.hoverStyle.Render(b.label)
+ }
+ return b.style.Render(b.label)
+}
+
diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go
new file mode 100644
index 000000000..f7581330a
--- /dev/null
+++ b/internal/tui/components/dialog/permission.go
@@ -0,0 +1,167 @@
+package dialog
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/kujtimiihoxha/termai/internal/permission"
+ "github.com/kujtimiihoxha/termai/internal/tui/components/core"
+ "github.com/kujtimiihoxha/termai/internal/tui/layout"
+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
+ "github.com/kujtimiihoxha/termai/internal/tui/util"
+
+ "github.com/charmbracelet/huh"
+)
+
+type PermissionAction string
+
+// Permission responses
+const (
+ PermissionAllow PermissionAction = "allow"
+ PermissionAllowForSession PermissionAction = "allow_session"
+ PermissionDeny PermissionAction = "deny"
+)
+
+// PermissionResponseMsg represents the user's response to a permission request
+type PermissionResponseMsg struct {
+ Permission permission.PermissionRequest
+ Action PermissionAction
+}
+
+// Width and height constants for the dialog
+var (
+ permissionWidth = 60
+ permissionHeight = 10
+)
+
+// PermissionDialog interface for permission dialog component
+type PermissionDialog interface {
+ tea.Model
+ layout.Sizeable
+ layout.Bindings
+}
+
+// permissionDialogCmp is the implementation of PermissionDialog
+type permissionDialogCmp struct {
+ form *huh.Form
+ content string
+ width int
+ height int
+ permission permission.PermissionRequest
+}
+
+func (p *permissionDialogCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+
+ // Process the form
+ form, cmd := p.form.Update(msg)
+ if f, ok := form.(*huh.Form); ok {
+ p.form = f
+ cmds = append(cmds, cmd)
+ }
+
+ if p.form.State == huh.StateCompleted {
+ // Get the selected action
+ action := p.form.GetString("action")
+
+ // Close the dialog and return the response
+ return p, tea.Batch(
+ util.CmdHandler(core.DialogCloseMsg{}),
+ util.CmdHandler(PermissionResponseMsg{Action: PermissionAction(action), Permission: p.permission}),
+ )
+ }
+
+ return p, tea.Batch(cmds...)
+}
+
+func (p *permissionDialogCmp) View() string {
+ contentStyle := lipgloss.NewStyle().
+ Width(p.width).
+ Padding(1, 0).
+ Foreground(styles.Text).
+ Align(lipgloss.Center)
+
+ return lipgloss.JoinVertical(
+ lipgloss.Center,
+ contentStyle.Render(p.content),
+ p.form.View(),
+ )
+}
+
+func (p *permissionDialogCmp) GetSize() (int, int) {
+ return p.width, p.height
+}
+
+func (p *permissionDialogCmp) SetSize(width int, height int) {
+ p.width = width
+ p.height = height
+}
+
+func (p *permissionDialogCmp) BindingKeys() []key.Binding {
+ return p.form.KeyBinds()
+}
+
+func newPermissionDialogCmp(permission permission.PermissionRequest, content string) PermissionDialog {
+ // Create a note field for displaying the content
+
+ // Create select field for the permission options
+ selectOption := huh.NewSelect[string]().
+ Key("action").
+ Options(
+ huh.NewOption("Allow", string(PermissionAllow)),
+ huh.NewOption("Allow for this session", string(PermissionAllowForSession)),
+ huh.NewOption("Deny", string(PermissionDeny)),
+ ).
+ Title("Permission Request")
+
+ // Apply theme
+ theme := styles.HuhTheme()
+
+ // Setup form width and height
+ form := huh.NewForm(huh.NewGroup(selectOption)).
+ WithWidth(permissionWidth - 2).
+ WithShowHelp(false).
+ WithTheme(theme).
+ WithShowErrors(false)
+
+ // Focus the form for immediate interaction
+ selectOption.Focus()
+
+ return &permissionDialogCmp{
+ permission: permission,
+ form: form,
+ content: content,
+ width: permissionWidth,
+ height: permissionHeight,
+ }
+}
+
+// NewPermissionDialogCmd creates a new permission dialog command
+func NewPermissionDialogCmd(permission permission.PermissionRequest, content string) tea.Cmd {
+ permDialog := newPermissionDialogCmp(permission, content)
+
+ // Create the dialog layout
+ dialogPane := layout.NewSinglePane(
+ permDialog.(*permissionDialogCmp),
+ layout.WithSignlePaneSize(permissionWidth+2, permissionHeight+2),
+ layout.WithSinglePaneBordered(true),
+ layout.WithSinglePaneFocusable(true),
+ layout.WithSinglePaneActiveColor(styles.Blue),
+ layout.WithSignlePaneBorderText(map[layout.BorderPosition]string{
+ layout.TopMiddleBorder: " Permission Required ",
+ }),
+ )
+
+ // Focus the dialog
+ dialogPane.Focus()
+
+ // Return the dialog command
+ return util.CmdHandler(core.DialogMsg{
+ Content: dialogPane,
+ })
+}
+
diff --git a/internal/tui/components/messages/message.go b/internal/tui/components/messages/message.go
new file mode 100644
index 000000000..619950850
--- /dev/null
+++ b/internal/tui/components/messages/message.go
@@ -0,0 +1,108 @@
+package messages
+
+import (
+ "fmt"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/cloudwego/eino/schema"
+ "github.com/kujtimiihoxha/termai/internal/message"
+ "github.com/kujtimiihoxha/termai/internal/tui/layout"
+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
+)
+
+const (
+ maxHeight = 10
+)
+
+type MessagesCmp interface {
+ tea.Model
+ layout.Focusable
+ layout.Bordered
+ layout.Sizeable
+}
+
+type messageCmp struct {
+ message message.Message
+ width int
+ height int
+ focused bool
+ expanded bool
+}
+
+func (m *messageCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (m *messageCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
+ return m, nil
+}
+
+func (m *messageCmp) View() string {
+ wrapper := layout.NewSinglePane(
+ m,
+ layout.WithSinglePaneBordered(true),
+ layout.WithSinglePaneFocusable(true),
+ layout.WithSinglePanePadding(1),
+ layout.WithSinglePaneActiveColor(m.borderColor()),
+ )
+ if m.focused {
+ wrapper.Focus()
+ }
+ wrapper.SetSize(m.width, m.height)
+ return wrapper.View()
+}
+
+func (m *messageCmp) Blur() tea.Cmd {
+ m.focused = false
+ return nil
+}
+
+func (m *messageCmp) borderColor() lipgloss.TerminalColor {
+ switch m.message.MessageData.Role {
+ case schema.Assistant:
+ return styles.Mauve
+ case schema.User:
+ return styles.Flamingo
+ }
+ return styles.Blue
+}
+
+func (m *messageCmp) BorderText() map[layout.BorderPosition]string {
+ role := ""
+ icon := ""
+ switch m.message.MessageData.Role {
+ case schema.Assistant:
+ role = "Assistant"
+ icon = styles.BotIcon
+ case schema.User:
+ role = "User"
+ icon = styles.UserIcon
+ }
+ return map[layout.BorderPosition]string{
+ layout.TopLeftBorder: fmt.Sprintf("%s %s ", role, icon),
+ }
+}
+
+func (m *messageCmp) Focus() tea.Cmd {
+ m.focused = true
+ return nil
+}
+
+func (m *messageCmp) IsFocused() bool {
+ return m.focused
+}
+
+func (m *messageCmp) GetSize() (int, int) {
+ return m.width, 0
+}
+
+func (m *messageCmp) SetSize(width int, height int) {
+ m.width = width
+}
+
+func NewMessageCmp(msg message.Message) MessagesCmp {
+ return &messageCmp{
+ message: msg,
+ }
+}
diff --git a/internal/tui/components/repl/editor.go b/internal/tui/components/repl/editor.go
index 8d795eb14..d0af8d2c5 100644
--- a/internal/tui/components/repl/editor.go
+++ b/internal/tui/components/repl/editor.go
@@ -5,9 +5,11 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
"github.com/cloudwego/eino/schema"
"github.com/kujtimiihoxha/termai/internal/app"
"github.com/kujtimiihoxha/termai/internal/tui/layout"
+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
"github.com/kujtimiihoxha/vimtea"
)
@@ -105,8 +107,12 @@ func (m *editorCmp) Blur() tea.Cmd {
}
func (m *editorCmp) BorderText() map[layout.BorderPosition]string {
+ title := "New Message"
+ if m.focused {
+ title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
+ }
return map[layout.BorderPosition]string{
- layout.TopLeftBorder: "New Message",
+ layout.TopLeftBorder: title,
}
}
@@ -148,7 +154,9 @@ func (m *editorCmp) BindingKeys() []key.Binding {
func NewEditorCmp(app *app.App) EditorCmp {
return &editorCmp{
- app: app,
- editor: vimtea.NewEditor(),
+ app: app,
+ editor: vimtea.NewEditor(
+ vimtea.WithFileName("message.md"),
+ ),
}
}
diff --git a/internal/tui/components/repl/messages.go b/internal/tui/components/repl/messages.go
index feddf7bf6..9b3c5bde8 100644
--- a/internal/tui/components/repl/messages.go
+++ b/internal/tui/components/repl/messages.go
@@ -1,15 +1,22 @@
package repl
import (
+ "fmt"
+ "slices"
+ "strings"
+
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
+ "github.com/cloudwego/eino/schema"
"github.com/kujtimiihoxha/termai/internal/app"
"github.com/kujtimiihoxha/termai/internal/message"
"github.com/kujtimiihoxha/termai/internal/pubsub"
"github.com/kujtimiihoxha/termai/internal/session"
"github.com/kujtimiihoxha/termai/internal/tui/layout"
+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
)
type MessagesCmp interface {
@@ -21,13 +28,15 @@ type MessagesCmp interface {
}
type messagesCmp struct {
- app *app.App
- messages []message.Message
- session session.Session
- viewport viewport.Model
- width int
- height int
- focused bool
+ app *app.App
+ messages []message.Message
+ session session.Session
+ viewport viewport.Model
+ mdRenderer *glamour.TermRenderer
+ width int
+ height int
+ focused bool
+ cachedView string
}
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -35,6 +44,8 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case pubsub.Event[message.Message]:
if msg.Type == pubsub.CreatedEvent {
m.messages = append(m.messages, msg.Payload)
+ m.renderView()
+ m.viewport.GotoBottom()
}
case pubsub.Event[session.Session]:
if msg.Type == pubsub.UpdatedEvent {
@@ -45,60 +56,182 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case SelectedSessionMsg:
m.session, _ = m.app.Sessions.Get(msg.SessionID)
m.messages, _ = m.app.Messages.List(m.session.ID)
+ m.renderView()
+ m.viewport.GotoBottom()
+ }
+ if m.focused {
+ u, cmd := m.viewport.Update(msg)
+ m.viewport = u
+ return m, cmd
}
return m, nil
}
-func (i *messagesCmp) View() string {
- stringMessages := make([]string, len(i.messages))
- for idx, msg := range i.messages {
- stringMessages[idx] = msg.MessageData.Content
+func borderColor(role schema.RoleType) lipgloss.TerminalColor {
+ switch role {
+ case schema.Assistant:
+ return styles.Mauve
+ case schema.User:
+ return styles.Rosewater
+ case schema.Tool:
+ return styles.Peach
}
- return lipgloss.JoinVertical(lipgloss.Top, stringMessages...)
+ return styles.Blue
+}
+
+func borderText(msgRole schema.RoleType, currentMessage int) map[layout.BorderPosition]string {
+ role := ""
+ icon := ""
+ switch msgRole {
+ case schema.Assistant:
+ role = "Assistant"
+ icon = styles.BotIcon
+ case schema.User:
+ role = "User"
+ icon = styles.UserIcon
+ }
+ return map[layout.BorderPosition]string{
+ layout.TopLeftBorder: lipgloss.NewStyle().
+ Padding(0, 1).
+ Bold(true).
+ Foreground(styles.Crust).
+ Background(borderColor(msgRole)).
+ Render(fmt.Sprintf("%s %s ", role, icon)),
+ layout.TopRightBorder: lipgloss.NewStyle().
+ Padding(0, 1).
+ Bold(true).
+ Foreground(styles.Crust).
+ Background(borderColor(msgRole)).
+ Render(fmt.Sprintf("#%d ", currentMessage)),
+ }
+}
+
+func (m *messagesCmp) renderView() {
+ stringMessages := make([]string, 0)
+ r, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
+ glamour.WithWordWrap(m.width-10),
+ glamour.WithEmoji(),
+ )
+ textStyle := lipgloss.NewStyle().Width(m.width - 4)
+ currentMessage := 1
+ for _, msg := range m.messages {
+ if msg.MessageData.Role == schema.Tool {
+ continue
+ }
+ content := msg.MessageData.Content
+ if content != "" {
+ content, _ = r.Render(msg.MessageData.Content)
+ stringMessages = append(stringMessages, layout.Borderize(
+ textStyle.Render(content),
+ layout.BorderOptions{
+ InactiveBorder: lipgloss.DoubleBorder(),
+ ActiveBorder: lipgloss.DoubleBorder(),
+ ActiveColor: borderColor(msg.MessageData.Role),
+ InactiveColor: borderColor(msg.MessageData.Role),
+ EmbeddedText: borderText(msg.MessageData.Role, currentMessage),
+ },
+ ))
+ currentMessage++
+ }
+ for _, toolCall := range msg.MessageData.ToolCalls {
+ resultInx := slices.IndexFunc(m.messages, func(m message.Message) bool {
+ return m.MessageData.ToolCallID == toolCall.ID
+ })
+ content := fmt.Sprintf("**Arguments**\n```json\n%s\n```\n", toolCall.Function.Arguments)
+ if resultInx == -1 {
+ content += "Running..."
+ } else {
+ result := m.messages[resultInx].MessageData.Content
+ if result != "" {
+ lines := strings.Split(result, "\n")
+ if len(lines) > 15 {
+ result = strings.Join(lines[:15], "\n")
+ }
+ content += fmt.Sprintf("**Result**\n```\n%s\n```\n", result)
+ if len(lines) > 15 {
+ content += fmt.Sprintf("\n\n *...%d lines are truncated* ", len(lines)-15)
+ }
+ }
+ }
+ content, _ = r.Render(content)
+ stringMessages = append(stringMessages, layout.Borderize(
+ textStyle.Render(content),
+ layout.BorderOptions{
+ InactiveBorder: lipgloss.DoubleBorder(),
+ ActiveBorder: lipgloss.DoubleBorder(),
+ ActiveColor: borderColor(schema.Tool),
+ InactiveColor: borderColor(schema.Tool),
+ EmbeddedText: map[layout.BorderPosition]string{
+ layout.TopLeftBorder: lipgloss.NewStyle().
+ Padding(0, 1).
+ Bold(true).
+ Foreground(styles.Crust).
+ Background(borderColor(schema.Tool)).
+ Render(
+ fmt.Sprintf("Tool [%s] %s ", toolCall.Function.Name, styles.ToolIcon),
+ ),
+ layout.TopRightBorder: lipgloss.NewStyle().
+ Padding(0, 1).
+ Bold(true).
+ Foreground(styles.Crust).
+ Background(borderColor(schema.Tool)).
+ Render(fmt.Sprintf("#%d ", currentMessage)),
+ },
+ },
+ ))
+ currentMessage++
+ }
+ }
+ m.viewport.SetContent(lipgloss.JoinVertical(lipgloss.Top, stringMessages...))
+}
+
+func (m *messagesCmp) View() string {
+ return lipgloss.NewStyle().Padding(1).Render(m.viewport.View())
}
-// BindingKeys implements MessagesCmp.
func (m *messagesCmp) BindingKeys() []key.Binding {
- return []key.Binding{}
+ return layout.KeyMapToSlice(m.viewport.KeyMap)
}
-// Blur implements MessagesCmp.
func (m *messagesCmp) Blur() tea.Cmd {
m.focused = false
return nil
}
-// BorderText implements MessagesCmp.
func (m *messagesCmp) BorderText() map[layout.BorderPosition]string {
title := m.session.Title
- if len(title) > 20 {
- title = title[:20] + "..."
+ titleWidth := m.width / 2
+ if len(title) > titleWidth {
+ title = title[:titleWidth] + "..."
+ }
+ if m.focused {
+ title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
}
return map[layout.BorderPosition]string{
- layout.TopLeftBorder: title,
+ layout.TopLeftBorder: title,
+ layout.BottomRightBorder: formatTokensAndCost(m.session.CompletionTokens+m.session.PromptTokens, m.session.Cost),
}
}
-// Focus implements MessagesCmp.
func (m *messagesCmp) Focus() tea.Cmd {
m.focused = true
return nil
}
-// GetSize implements MessagesCmp.
func (m *messagesCmp) GetSize() (int, int) {
return m.width, m.height
}
-// IsFocused implements MessagesCmp.
func (m *messagesCmp) IsFocused() bool {
return m.focused
}
-// SetSize implements MessagesCmp.
func (m *messagesCmp) SetSize(width int, height int) {
m.width = width
m.height = height
+ m.viewport.Width = width - 2 // padding
+ m.viewport.Height = height - 2 // padding
}
func (m *messagesCmp) Init() tea.Cmd {
@@ -109,5 +242,6 @@ func NewMessagesCmp(app *app.App) MessagesCmp {
return &messagesCmp{
app: app,
messages: []message.Message{},
+ viewport: viewport.New(0, 0),
}
}
diff --git a/internal/tui/components/repl/sessions.go b/internal/tui/components/repl/sessions.go
index 5d2411fb6..0f208ced9 100644
--- a/internal/tui/components/repl/sessions.go
+++ b/internal/tui/components/repl/sessions.go
@@ -7,6 +7,7 @@ import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
"github.com/kujtimiihoxha/termai/internal/app"
"github.com/kujtimiihoxha/termai/internal/pubsub"
"github.com/kujtimiihoxha/termai/internal/session"
@@ -160,8 +161,13 @@ func (i *sessionsCmp) BorderText() map[layout.BorderPosition]string {
current,
totalCount,
)
+
+ title := "Sessions"
+ if i.focused {
+ title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
+ }
return map[layout.BorderPosition]string{
- layout.TopMiddleBorder: "Sessions",
+ layout.TopMiddleBorder: title,
layout.BottomMiddleBorder: pageInfo,
}
}