From 68db9deea8fbea44450f1557cd92a6b5f5844d6b Mon Sep 17 00:00:00 2001 From: db123-ai Date: Wed, 10 Jun 2026 00:49:24 +0330 Subject: [PATCH] feat(channel): add Bubble Tea TUI chat interface and Matrix adapter stub --- internal/channel/matrix.go | 33 ++++++++ internal/channel/tui.go | 160 +++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 internal/channel/matrix.go create mode 100644 internal/channel/tui.go diff --git a/internal/channel/matrix.go b/internal/channel/matrix.go new file mode 100644 index 0000000..57f8b70 --- /dev/null +++ b/internal/channel/matrix.go @@ -0,0 +1,33 @@ +package channel + +import ( + "context" + "log/slog" + + "git.db123.ir/db123/divacode/internal/agent" + "git.db123.ir/db123/divacode/internal/config" +) + +type MatrixClient struct { + cfg *config.Config + agent *agent.Agent +} + +func NewMatrixClient(cfg *config.Config, a *agent.Agent) *MatrixClient { + return &MatrixClient{ + cfg: cfg, + agent: a, + } +} + +func (mc *MatrixClient) Start(ctx context.Context) error { + if !mc.cfg.MatrixEnabled { + slog.Info("matrix channel disabled") + return nil + } + slog.Info("matrix client connecting", + "homeserver", mc.cfg.MatrixHomeserver, + "user", mc.cfg.MatrixUser, + ) + return nil +} diff --git a/internal/channel/tui.go b/internal/channel/tui.go new file mode 100644 index 0000000..d657f2d --- /dev/null +++ b/internal/channel/tui.go @@ -0,0 +1,160 @@ +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} + } +}