package channel import ( "context" "fmt" "log/slog" "strings" "git.db123.ir/db123/divacode/internal/agent" "git.db123.ir/db123/divacode/pkg/types" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( userStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#7C3AED")). Bold(true) assistantStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#10B981")). Bold(true) inputStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("#374151")) ) type sessionState int const ( stateReady sessionState = iota stateWaiting ) type model struct { agent *agent.Agent ctx context.Context cancel context.CancelFunc viewport viewport.Model input string state sessionState messages []types.Message ready bool } func NewTUIModel(a *agent.Agent) tea.Model { ctx, cancel := context.WithCancel(context.Background()) return &model{ agent: a, ctx: ctx, cancel: cancel, state: stateReady, } } func (m *model) Init() tea.Cmd { return nil } func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: if !m.ready { m.viewport = viewport.New(msg.Width, msg.Height-3) m.viewport.YPosition = 0 m.ready = true } else { m.viewport.Width = msg.Width m.viewport.Height = msg.Height - 3 } return m, nil case tea.KeyMsg: switch msg.String() { case "ctrl+c", "ctrl+q": m.cancel() return m, tea.Quit case "enter": if m.state == stateReady && strings.TrimSpace(m.input) != "" { input := strings.TrimSpace(m.input) m.input = "" m.state = stateWaiting return m, m.sendMessage(input) } case "backspace": if len(m.input) > 0 { m.input = m.input[:len(m.input)-1] } default: if m.state == stateReady { m.input += msg.String() } } case responseMsg: m.state = stateReady if msg.err != nil { m.messages = append(m.messages, types.Message{ Role: types.RoleSystem, Content: fmt.Sprintf("error: %v", msg.err), }) } else { m.messages = m.agent.Messages() } m.viewport.GotoBottom() } return m, nil } func (m *model) View() string { if !m.ready { return "initializing divacode..." } var b strings.Builder for _, msg := range m.messages { prefix := "sys" style := lipgloss.NewStyle() switch msg.Role { case types.RoleUser: prefix = "you" style = userStyle case types.RoleAssistant: prefix = "diva" style = assistantStyle default: prefix = "system" } b.WriteString(style.Render(prefix+": ") + msg.Content + "\n\n") } m.viewport.SetContent(b.String()) prompt := "> " if m.state == stateWaiting { prompt = "waiting... " } return fmt.Sprintf("%s\n%s%s", m.viewport.View(), inputStyle.Render(prompt), m.input) } type responseMsg struct { err error } func (m *model) sendMessage(content string) tea.Cmd { return func() tea.Msg { slog.Info("tui message", "content", content) _, err := m.agent.HandleMessage(m.ctx, content) return responseMsg{err: err} } }