diff --git a/internal/tui/model.go b/internal/tui/model.go index 216afcc..6216313 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -212,17 +212,18 @@ func (m *Model) updateLayout() { rightWidth := 42 leftWidth := m.width - rightWidth - 3 - headerHeight := 3 - mainHeight := m.height - headerHeight - 1 - inputHeight := 5 - chatHeight := mainHeight - inputHeight - if chatHeight < 10 { - chatHeight = 10 + + headerHeight := 2 + mainHeight := m.height - headerHeight + if mainHeight < 10 { + mainHeight = 10 + } + chatHeight := mainHeight - 9 + if chatHeight < 5 { + chatHeight = 5 } m.textarea.SetWidth(leftWidth) - m.viewport.Width = leftWidth - m.viewport.Height = chatHeight if !m.ready { m.viewport = viewport.New(leftWidth, chatHeight) @@ -265,33 +266,18 @@ func (m Model) View() string { return "Initializing..." } - rightWidth := 42 - leftWidth := m.width - rightWidth - 3 + rightWidth := 40 + leftWidth := m.width - rightWidth - 2 header := m.renderHeader() - headerHeight := lipgloss.Height(header) - mainHeight := m.height - headerHeight - inputHeight := 5 - chatHeight := mainHeight - inputHeight - if chatHeight < 10 { - chatHeight = 10 - } - chatBox := boxStyle.Width(leftWidth).Height(chatHeight).Render(m.viewport.View()) + chatBox := boxStyle.Width(leftWidth).Height(m.height - 8).Render(m.viewport.View()) inputBox := boxStyle.Width(leftWidth).Render(m.textarea.View()) leftPanel := lipgloss.JoinVertical(lipgloss.Left, chatBox, inputBox) stats := m.renderStats() agents := m.renderAgents() - - statsHeight := lipgloss.Height(stats) - agentsHeight := lipgloss.Height(agents) - remainingHeight := mainHeight - statsHeight - agentsHeight - 2 - if remainingHeight < 3 { - remainingHeight = 3 - } - - empty := boxStyle.Width(rightWidth - 4).Height(remainingHeight).Render("") + empty := boxStyle.Width(rightWidth - 2).Height(m.height - lipgloss.Height(stats) - lipgloss.Height(agents) - 5).Render("") rightPanel := lipgloss.JoinVertical(lipgloss.Left, stats, agents, empty) if m.loading { @@ -299,19 +285,21 @@ func (m Model) View() string { } body := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) - return lipgloss.JoinVertical(lipgloss.Left, header, body) + return header + "\n" + body } func (m Model) renderHeader() string { version := "v0.1.0" - title := titleStyle.Render("orca.agent ") + mutedStyle.Render(version) + title := "orca.agent " + version + line := strings.Repeat("─", m.width-2) return lipgloss.NewStyle(). Width(m.width). Padding(0, 1). - BorderBottom(true). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colors.border)). - Render(title) + Bold(true). + Foreground(lipgloss.Color(colors.primary)). + Render(title) + "\n" + lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.border)). + Render(line) } func (m Model) renderStats() string { @@ -319,16 +307,22 @@ func (m Model) renderStats() string { b.WriteString(titleStyle.Render("Statistics") + "\n\n") tools := 0 - if tm := m.kernel.ToolManager(); tm != nil { - tools = tm.Count() + if m.kernel != nil { + if tm := m.kernel.ToolManager(); tm != nil { + tools = tm.Count() + } } skills := 0 - if sm := m.kernel.SkillManager(); sm != nil { - skills = len(sm.ListSkills()) + if m.kernel != nil { + if sm := m.kernel.SkillManager(); sm != nil { + skills = len(sm.ListSkills()) + } } agents := 0 - if as := m.kernel.ActorSystem(); as != nil { - agents = as.AgentCount() + if m.kernel != nil { + if as := m.kernel.ActorSystem(); as != nil { + agents = as.AgentCount() + } } b.WriteString(statLabelStyle.Render("Tools: ")) @@ -345,15 +339,17 @@ func (m Model) renderAgents() string { var b strings.Builder b.WriteString(titleStyle.Render("Active Agents") + "\n\n") - if as := m.kernel.ActorSystem(); as != nil { - for _, info := range as.AgentInfos() { - status := "idle" - style := idleAgentStyle - if info.Status == actor.StatusProcessing || m.agentRuns[info.ID] { - status = "running" - style = activeAgentStyle + if m.kernel != nil { + if as := m.kernel.ActorSystem(); as != nil { + for _, info := range as.AgentInfos() { + status := "idle" + style := idleAgentStyle + if info.Status == actor.StatusProcessing || m.agentRuns[info.ID] { + status = "running" + style = activeAgentStyle + } + b.WriteString(fmt.Sprintf("• %s: %s\n", info.ID, style.Render(status))) } - b.WriteString(fmt.Sprintf("• %s: %s\n", info.ID, style.Render(status))) } } diff --git a/internal/tui/model_test.go b/internal/tui/model_test.go index cf7e218..926e57b 100644 --- a/internal/tui/model_test.go +++ b/internal/tui/model_test.go @@ -1,8 +1,11 @@ package tui import ( + "strings" "testing" "time" + + "github.com/charmbracelet/bubbles/textarea" ) func TestEventWriter(t *testing.T) { @@ -74,3 +77,26 @@ func TestModelFormatMessage(t *testing.T) { t.Error("system message should not be empty") } } + +func TestViewContainsHeader(t *testing.T) { + ta := textarea.New() + ta.ShowLineNumbers = false + ta.KeyMap.InsertNewline.SetEnabled(false) + + m := Model{ + width: 120, + height: 40, + textarea: ta, + messages: []ChatMessage{}, + events: make(chan Event, 10), + agentRuns: make(map[string]bool), + } + m.updateLayout() + view := m.View() + if view == "Initializing..." { + t.Fatal("view should not be initializing") + } + if !strings.Contains(view, "orca.agent") { + t.Errorf("view should contain 'orca.agent', got:\n%s", view[:min(200, len(view))]) + } +}