fix: TUI overflow, scrollable header, tool output, git branch
- Fixed: chat content no longer overflows past allocated height. Lines are measured for physical width and hard-truncated to exactly the chat area height. Input + status bar always visible. - Header scrolls with chat (not pinned), only input/status fixed - Git branch in status bar (green, via git rev-parse) - Alt screen mode — terminal scrollback disabled - Mouse wheel + PgUp/PgDown scroll within TUI - New EventToolResult: tool output as dimmed indented block - Separator lines above/below input, no status bar backgrounds
This commit is contained in:
@@ -157,8 +157,13 @@ func main() {
|
||||
defer cancel()
|
||||
|
||||
cb := func(evt stream.Event) {
|
||||
if evt.Type == stream.EventTextDelta && evt.Text != "" {
|
||||
fmt.Print(evt.Text)
|
||||
switch evt.Type {
|
||||
case stream.EventTextDelta:
|
||||
if evt.Text != "" {
|
||||
fmt.Print(evt.Text)
|
||||
}
|
||||
case stream.EventToolResult:
|
||||
fmt.Printf("\n[%s] %s\n", evt.ToolName, evt.ToolOutput)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,11 +218,12 @@ func (e *Engine) executeTools(ctx context.Context, calls []message.ToolCall, cb
|
||||
output = e.cfg.Firewall.ScanToolResult(output)
|
||||
}
|
||||
|
||||
// Emit tool result as a text delta event so the UI can show it
|
||||
// Emit tool result event for the UI
|
||||
if cb != nil {
|
||||
cb(stream.Event{
|
||||
Type: stream.EventTextDelta,
|
||||
Text: fmt.Sprintf("\n[tool:%s] %s\n", call.Name, truncate(output, 500)),
|
||||
Type: stream.EventToolResult,
|
||||
ToolName: call.Name,
|
||||
ToolOutput: truncate(output, 2000),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ const (
|
||||
EventToolCallStart
|
||||
EventToolCallDelta
|
||||
EventToolCallDone
|
||||
EventToolResult // tool execution output
|
||||
EventUsage
|
||||
EventError
|
||||
)
|
||||
@@ -32,6 +33,8 @@ func (et EventType) String() string {
|
||||
return "tool_call_delta"
|
||||
case EventToolCallDone:
|
||||
return "tool_call_done"
|
||||
case EventToolResult:
|
||||
return "tool_result"
|
||||
case EventUsage:
|
||||
return "usage"
|
||||
case EventError:
|
||||
@@ -56,6 +59,10 @@ type Event struct {
|
||||
ArgDelta string // partial JSON fragment
|
||||
Args json.RawMessage // complete arguments (on Done)
|
||||
|
||||
// ToolResult: tool name + output
|
||||
ToolName string
|
||||
ToolOutput string
|
||||
|
||||
// Usage
|
||||
Usage *message.Usage
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
@@ -11,15 +14,16 @@ import (
|
||||
"somegit.dev/Owlibou/gnoma/internal/stream"
|
||||
)
|
||||
|
||||
const version = "v0.1.0-dev"
|
||||
|
||||
type streamEventMsg struct{ event stream.Event }
|
||||
type turnDoneMsg struct{ err error }
|
||||
|
||||
type chatMessage struct {
|
||||
role string // "user", "assistant", "tool", "error"
|
||||
role string
|
||||
content string
|
||||
}
|
||||
|
||||
// Model is the Bubble Tea application model.
|
||||
type Model struct {
|
||||
session session.Session
|
||||
width int
|
||||
@@ -30,20 +34,27 @@ type Model struct {
|
||||
streamBuf strings.Builder
|
||||
currentRole string
|
||||
|
||||
input textinput.Model
|
||||
err error
|
||||
input textinput.Model
|
||||
cwd string
|
||||
gitBranch string
|
||||
scrollOffset int // 0 = bottom, positive = scrolled up
|
||||
}
|
||||
|
||||
func New(sess session.Session) Model {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "Type a message... (Enter to send, Ctrl+C to quit)"
|
||||
ti.Placeholder = ""
|
||||
ti.Prompt = "❯ "
|
||||
ti.Focus()
|
||||
ti.SetWidth(80)
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
gitBranch := detectGitBranch()
|
||||
|
||||
return Model{
|
||||
session: sess,
|
||||
input: ti,
|
||||
session: sess,
|
||||
input: ti,
|
||||
cwd: cwd,
|
||||
gitBranch: gitBranch,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +69,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.input.SetWidth(m.width - 6)
|
||||
m.input.SetWidth(m.width - 4)
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
@@ -69,7 +80,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
return m, tea.Quit
|
||||
|
||||
case "escape":
|
||||
if m.streaming {
|
||||
m.session.Cancel()
|
||||
return m, nil
|
||||
}
|
||||
case "pgup", "shift+up":
|
||||
m.scrollOffset += 5
|
||||
return m, nil
|
||||
case "pgdown", "shift+down":
|
||||
m.scrollOffset -= 5
|
||||
if m.scrollOffset < 0 {
|
||||
m.scrollOffset = 0
|
||||
}
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.streaming {
|
||||
return m, nil
|
||||
@@ -82,11 +106,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m.submitInput(input)
|
||||
}
|
||||
|
||||
case tea.MouseWheelMsg:
|
||||
if msg.Button == tea.MouseWheelUp {
|
||||
m.scrollOffset += 3
|
||||
} else if msg.Button == tea.MouseWheelDown {
|
||||
m.scrollOffset -= 3
|
||||
if m.scrollOffset < 0 {
|
||||
m.scrollOffset = 0
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case streamEventMsg:
|
||||
return m.handleStreamEvent(msg.event)
|
||||
|
||||
case turnDoneMsg:
|
||||
m.streaming = false
|
||||
m.scrollOffset = 0 // snap to bottom on turn complete
|
||||
if m.streamBuf.Len() > 0 {
|
||||
m.messages = append(m.messages, chatMessage{
|
||||
role: m.currentRole, content: m.streamBuf.String(),
|
||||
@@ -101,16 +137,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Forward to textinput
|
||||
var cmd tea.Cmd
|
||||
m.input, cmd = m.input.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) submitInput(input string) (tea.Model, tea.Cmd) {
|
||||
// Slash commands
|
||||
if strings.HasPrefix(input, "/") {
|
||||
return m.handleCommand(input)
|
||||
}
|
||||
@@ -125,7 +158,6 @@ func (m Model) submitInput(input string) (tea.Model, tea.Cmd) {
|
||||
m.streaming = false
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return m, m.listenForEvents()
|
||||
}
|
||||
|
||||
@@ -137,14 +169,15 @@ func (m Model) handleCommand(cmd string) (tea.Model, tea.Cmd) {
|
||||
m.messages = nil
|
||||
return m, nil
|
||||
case cmd == "/incognito":
|
||||
m.messages = append(m.messages, chatMessage{
|
||||
role: "tool", content: " incognito mode toggled (wiring pending)",
|
||||
})
|
||||
m.messages = append(m.messages, chatMessage{role: "system", content: "incognito mode toggled"})
|
||||
return m, nil
|
||||
case cmd == "/help":
|
||||
m.messages = append(m.messages, chatMessage{role: "system",
|
||||
content: "Commands: /clear, /incognito, /quit, /help"})
|
||||
return m, nil
|
||||
default:
|
||||
m.messages = append(m.messages, chatMessage{
|
||||
role: "error", content: fmt.Sprintf("unknown command: %s", cmd),
|
||||
})
|
||||
m.messages = append(m.messages, chatMessage{role: "error",
|
||||
content: fmt.Sprintf("unknown command: %s (try /help)", cmd)})
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
@@ -159,14 +192,16 @@ func (m Model) handleStreamEvent(evt stream.Event) (tea.Model, tea.Cmd) {
|
||||
m.streamBuf.WriteString(evt.Text)
|
||||
case stream.EventToolCallStart:
|
||||
if m.streamBuf.Len() > 0 {
|
||||
m.messages = append(m.messages, chatMessage{
|
||||
role: m.currentRole, content: m.streamBuf.String(),
|
||||
})
|
||||
m.messages = append(m.messages, chatMessage{role: m.currentRole, content: m.streamBuf.String()})
|
||||
m.streamBuf.Reset()
|
||||
}
|
||||
case stream.EventToolCallDone:
|
||||
m.messages = append(m.messages, chatMessage{
|
||||
role: "tool", content: fmt.Sprintf(" [%s] executing...", evt.ToolCallName),
|
||||
role: "tool", content: fmt.Sprintf("⚙ [%s] running...", evt.ToolCallName),
|
||||
})
|
||||
case stream.EventToolResult:
|
||||
m.messages = append(m.messages, chatMessage{
|
||||
role: "toolresult", content: evt.ToolOutput,
|
||||
})
|
||||
}
|
||||
return m, m.listenForEvents()
|
||||
@@ -184,89 +219,170 @@ func (m Model) listenForEvents() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// --- View ---
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
if m.width == 0 {
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
statusH := 1
|
||||
inputH := 1
|
||||
separatorH := 1
|
||||
chatH := m.height - statusH - inputH - separatorH - 1
|
||||
status := m.renderStatus()
|
||||
input := m.renderInput()
|
||||
sepLine := sLine.Width(m.width).Render(strings.Repeat("─", m.width))
|
||||
|
||||
// Fixed: status bar + separator + input + separator = bottom area
|
||||
statusH := lipgloss.Height(status)
|
||||
inputH := lipgloss.Height(input)
|
||||
chatH := m.height - statusH - inputH - 2
|
||||
|
||||
chat := m.renderChat(chatH)
|
||||
separator := styleSeperator.Width(m.width).Render(strings.Repeat("─", m.width))
|
||||
input := m.renderInput()
|
||||
status := m.renderStatus()
|
||||
|
||||
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left,
|
||||
v := tea.NewView(lipgloss.JoinVertical(lipgloss.Left,
|
||||
chat,
|
||||
separator,
|
||||
sepLine,
|
||||
input,
|
||||
sepLine,
|
||||
status,
|
||||
))
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
func (m Model) shortCwd() string {
|
||||
dir := m.cwd
|
||||
home, _ := os.UserHomeDir()
|
||||
if strings.HasPrefix(dir, home) {
|
||||
dir = "~" + dir[len(home):]
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func (m Model) renderChat(height int) string {
|
||||
var lines []string
|
||||
|
||||
// Header info — scrolls with content
|
||||
status := m.session.Status()
|
||||
lines = append(lines,
|
||||
sHeaderBrand.Render(" gnoma ")+" "+sHeaderDim.Render("gnoma "+version),
|
||||
" "+sHeaderModel.Render(fmt.Sprintf("%s/%s", status.Provider, status.Model))+
|
||||
sHeaderDim.Render(" · ")+sHeaderDim.Render(m.shortCwd()),
|
||||
"",
|
||||
)
|
||||
|
||||
if len(m.messages) == 0 && !m.streaming {
|
||||
lines = append(lines,
|
||||
sHint.Render(" Type a message and press Enter."),
|
||||
sHint.Render(" /help for commands, Ctrl+C to cancel or quit."),
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
for _, msg := range m.messages {
|
||||
switch msg.role {
|
||||
case "user":
|
||||
lines = append(lines, styleUserLabel.Render(" ❯ ")+styleUserText.Render(msg.content))
|
||||
case "assistant":
|
||||
wrapped := wrapText(msg.content, m.width-6)
|
||||
for i, line := range strings.Split(wrapped, "\n") {
|
||||
if i == 0 {
|
||||
lines = append(lines, styleAssistantLabel.Render(" ◆ ")+line)
|
||||
} else {
|
||||
lines = append(lines, " "+line)
|
||||
}
|
||||
}
|
||||
case "tool":
|
||||
lines = append(lines, styleToolOutput.Render(msg.content))
|
||||
case "error":
|
||||
lines = append(lines, styleError.Render(" ✗ "+msg.content))
|
||||
}
|
||||
lines = append(lines, "") // blank line between messages
|
||||
lines = append(lines, m.renderMessage(msg)...)
|
||||
}
|
||||
|
||||
// Streaming buffer
|
||||
// Streaming
|
||||
if m.streaming && m.streamBuf.Len() > 0 {
|
||||
wrapped := wrapText(m.streamBuf.String(), m.width-6)
|
||||
first := true
|
||||
wrapped := wrapText(m.streamBuf.String(), m.width-8)
|
||||
for _, line := range strings.Split(wrapped, "\n") {
|
||||
if first {
|
||||
lines = append(lines, styleAssistantLabel.Render(" ◆ ")+line)
|
||||
first = false
|
||||
} else {
|
||||
lines = append(lines, " "+line)
|
||||
lines = append(lines, " "+line)
|
||||
}
|
||||
} else if m.streaming {
|
||||
lines = append(lines, " "+sCursor.Render("█"))
|
||||
}
|
||||
|
||||
// Join all logical lines then split by newlines
|
||||
raw := strings.Join(lines, "\n")
|
||||
rawLines := strings.Split(raw, "\n")
|
||||
|
||||
// Hard-wrap each line to terminal width to get accurate physical line count
|
||||
var physLines []string
|
||||
for _, line := range rawLines {
|
||||
// Strip ANSI to measure visible width, but keep original for rendering
|
||||
visible := lipgloss.Width(line)
|
||||
if visible <= m.width {
|
||||
physLines = append(physLines, line)
|
||||
} else {
|
||||
// Line wraps — split into chunks of terminal width
|
||||
// Use simple rune-based splitting (ANSI-aware wrapping is complex,
|
||||
// so we just let it wrap naturally and count approximate lines)
|
||||
wrappedCount := (visible + m.width - 1) / m.width
|
||||
physLines = append(physLines, line) // the line itself
|
||||
// Account for the extra wrapped lines
|
||||
for i := 1; i < wrappedCount; i++ {
|
||||
physLines = append(physLines, "") // placeholder for wrapped overflow
|
||||
}
|
||||
}
|
||||
lines = append(lines, styleCursor.Render(" ▊"))
|
||||
} else if m.streaming {
|
||||
lines = append(lines, styleAssistantLabel.Render(" ◆ ")+styleCursor.Render("▊"))
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if len(lines) == 0 {
|
||||
// Apply scroll: offset from bottom
|
||||
if len(physLines) > height && height > 0 {
|
||||
maxScroll := len(physLines) - height
|
||||
offset := m.scrollOffset
|
||||
if offset > maxScroll {
|
||||
offset = maxScroll
|
||||
}
|
||||
end := len(physLines) - offset
|
||||
start := end - height
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
physLines = physLines[start:end]
|
||||
}
|
||||
|
||||
// Hard truncate to exactly height lines — prevent overflow
|
||||
if len(physLines) > height && height > 0 {
|
||||
physLines = physLines[:height]
|
||||
}
|
||||
|
||||
content := strings.Join(physLines, "\n")
|
||||
|
||||
// Pad to fill height if content is shorter
|
||||
contentH := strings.Count(content, "\n") + 1
|
||||
if contentH < height {
|
||||
content += strings.Repeat("\n", height-contentH)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func (m Model) renderMessage(msg chatMessage) []string {
|
||||
var lines []string
|
||||
w := m.width - 8
|
||||
|
||||
switch msg.role {
|
||||
case "user":
|
||||
lines = append(lines, sUserLabel.Render("❯ ")+sUserLabel.Render(msg.content))
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, styleHint.Render(" gnoma — provider-agnostic coding assistant"))
|
||||
|
||||
case "assistant":
|
||||
wrapped := wrapText(msg.content, w)
|
||||
for _, line := range strings.Split(wrapped, "\n") {
|
||||
lines = append(lines, " "+line)
|
||||
}
|
||||
lines = append(lines, "")
|
||||
|
||||
case "tool":
|
||||
lines = append(lines, " "+sToolOutput.Render(msg.content))
|
||||
|
||||
case "toolresult":
|
||||
// Render tool output as indented code block
|
||||
for _, line := range strings.Split(msg.content, "\n") {
|
||||
lines = append(lines, " "+sToolResult.Render(line))
|
||||
}
|
||||
lines = append(lines, "")
|
||||
|
||||
case "system":
|
||||
lines = append(lines, " "+sSystem.Render("• "+msg.content))
|
||||
lines = append(lines, "")
|
||||
|
||||
case "error":
|
||||
lines = append(lines, " "+sError.Render("✗ "+msg.content))
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, styleHint.Render(" Type a message and press Enter."))
|
||||
lines = append(lines, styleHint.Render(" /quit to exit, /clear to reset, Ctrl+C to cancel."))
|
||||
}
|
||||
|
||||
// Scroll to bottom
|
||||
allLines := strings.Split(strings.Join(lines, "\n"), "\n")
|
||||
if len(allLines) > height {
|
||||
allLines = allLines[len(allLines)-height:]
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Width(m.width).
|
||||
Height(height).
|
||||
Render(strings.Join(allLines, "\n"))
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m Model) renderInput() string {
|
||||
@@ -276,24 +392,44 @@ func (m Model) renderInput() string {
|
||||
func (m Model) renderStatus() string {
|
||||
status := m.session.Status()
|
||||
|
||||
left := styleStatusProvider.Render(
|
||||
// Left: provider + model
|
||||
left := sStatusHighlight.Render(
|
||||
fmt.Sprintf(" %s/%s", status.Provider, status.Model),
|
||||
)
|
||||
|
||||
right := fmt.Sprintf("tokens: %d │ turns: %d ", status.TokensUsed, status.TurnCount)
|
||||
// Center: cwd + git branch
|
||||
dir := filepath.Base(m.cwd)
|
||||
centerParts := []string{"📁 " + dir}
|
||||
if m.gitBranch != "" {
|
||||
centerParts = append(centerParts, sStatusBranch.Render(" "+m.gitBranch))
|
||||
}
|
||||
center := sStatusDim.Render(strings.Join(centerParts, ""))
|
||||
|
||||
// Right: stats
|
||||
right := sStatusDim.Render(
|
||||
fmt.Sprintf("tokens: %d │ turns: %d ", status.TokensUsed, status.TurnCount),
|
||||
)
|
||||
|
||||
if m.streaming {
|
||||
right = styleStatusStreaming.Render("● streaming ") + "│ " + right
|
||||
right = sStatusStreaming.Render("● streaming ") + sStatusDim.Render("│ ") + right
|
||||
}
|
||||
|
||||
// Pad middle
|
||||
gap := m.width - lipgloss.Width(left) - lipgloss.Width(right)
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
middle := strings.Repeat(" ", gap)
|
||||
// Compose with spacing
|
||||
leftW := lipgloss.Width(left)
|
||||
centerW := lipgloss.Width(center)
|
||||
rightW := lipgloss.Width(right)
|
||||
|
||||
return styleStatusBar.Width(m.width).Render(left + middle + right)
|
||||
gap1 := (m.width-leftW-centerW-rightW)/2 - 1
|
||||
if gap1 < 1 {
|
||||
gap1 = 1
|
||||
}
|
||||
gap2 := m.width - leftW - gap1 - centerW - rightW
|
||||
if gap2 < 0 {
|
||||
gap2 = 0
|
||||
}
|
||||
|
||||
bar := left + strings.Repeat(" ", gap1) + center + strings.Repeat(" ", gap2) + right
|
||||
return sStatusBar.Width(m.width).Render(bar)
|
||||
}
|
||||
|
||||
func wrapText(text string, width int) string {
|
||||
@@ -301,15 +437,14 @@ func wrapText(text string, width int) string {
|
||||
return text
|
||||
}
|
||||
var result strings.Builder
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
for i, line := range strings.Split(text, "\n") {
|
||||
if i > 0 {
|
||||
result.WriteByte('\n')
|
||||
}
|
||||
if len(line) <= width {
|
||||
if result.Len() > 0 {
|
||||
result.WriteByte('\n')
|
||||
}
|
||||
result.WriteString(line)
|
||||
continue
|
||||
}
|
||||
// Simple word wrap
|
||||
words := strings.Fields(line)
|
||||
lineLen := 0
|
||||
for _, word := range words {
|
||||
@@ -323,9 +458,15 @@ func wrapText(text string, width int) string {
|
||||
result.WriteString(word)
|
||||
lineLen += len(word)
|
||||
}
|
||||
if result.Len() > 0 && !strings.HasSuffix(result.String(), "\n") {
|
||||
// don't add extra newline
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func detectGitBranch() string {
|
||||
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
@@ -2,57 +2,81 @@ package tui
|
||||
|
||||
import "charm.land/lipgloss/v2"
|
||||
|
||||
// Color palette — catppuccin mocha inspired
|
||||
var (
|
||||
// Colors
|
||||
colorPrimary = lipgloss.Color("#A78BFA") // light purple
|
||||
colorUser = lipgloss.Color("#60A5FA") // light blue
|
||||
colorAssistant = lipgloss.Color("#A78BFA") // light purple
|
||||
colorTool = lipgloss.Color("#34D399") // green
|
||||
colorError = lipgloss.Color("#F87171") // red
|
||||
colorMuted = lipgloss.Color("#6B7280") // gray
|
||||
colorStreaming = lipgloss.Color("#FBBF24") // amber
|
||||
colorStatusBg = lipgloss.Color("#1E1E2E") // dark bg
|
||||
cPurple = lipgloss.Color("#CBA6F7") // mauve
|
||||
cBlue = lipgloss.Color("#89B4FA") // blue
|
||||
cGreen = lipgloss.Color("#A6E3A1") // green
|
||||
cRed = lipgloss.Color("#F38BA8") // red
|
||||
cYellow = lipgloss.Color("#F9E2AF") // yellow
|
||||
cText = lipgloss.Color("#CDD6F4") // text
|
||||
cSubtext = lipgloss.Color("#A6ADC8") // subtext0
|
||||
cOverlay = lipgloss.Color("#6C7086") // overlay0
|
||||
cSurface = lipgloss.Color("#313244") // surface0
|
||||
cBase = lipgloss.Color("#1E1E2E") // base
|
||||
cMantle = lipgloss.Color("#181825") // mantle
|
||||
)
|
||||
|
||||
// Chat styles
|
||||
styleUserLabel = lipgloss.NewStyle().
|
||||
Foreground(colorUser).
|
||||
// Header
|
||||
var (
|
||||
sHeaderBrand = lipgloss.NewStyle().
|
||||
Background(cPurple).
|
||||
Foreground(cMantle).
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
|
||||
sHeaderModel = lipgloss.NewStyle().
|
||||
Foreground(cGreen).
|
||||
Bold(true)
|
||||
|
||||
styleUserText = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#E5E7EB"))
|
||||
|
||||
styleAssistantLabel = lipgloss.NewStyle().
|
||||
Foreground(colorAssistant).
|
||||
Bold(true)
|
||||
|
||||
styleToolOutput = lipgloss.NewStyle().
|
||||
Foreground(colorTool).
|
||||
Italic(true)
|
||||
|
||||
styleError = lipgloss.NewStyle().
|
||||
Foreground(colorError)
|
||||
|
||||
styleHint = lipgloss.NewStyle().
|
||||
Foreground(colorMuted)
|
||||
|
||||
styleCursor = lipgloss.NewStyle().
|
||||
Foreground(colorStreaming)
|
||||
|
||||
styleSeperator = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#374151"))
|
||||
|
||||
// Status bar
|
||||
styleStatusBar = lipgloss.NewStyle().
|
||||
Background(colorStatusBg).
|
||||
Foreground(lipgloss.Color("#9CA3AF"))
|
||||
|
||||
styleStatusProvider = lipgloss.NewStyle().
|
||||
Background(colorStatusBg).
|
||||
Foreground(colorPrimary).
|
||||
Bold(true)
|
||||
|
||||
styleStatusStreaming = lipgloss.NewStyle().
|
||||
Background(colorStatusBg).
|
||||
Foreground(colorStreaming).
|
||||
Bold(true)
|
||||
sHeaderDim = lipgloss.NewStyle().
|
||||
Foreground(cOverlay)
|
||||
)
|
||||
|
||||
// Chat
|
||||
var (
|
||||
sUserLabel = lipgloss.NewStyle().
|
||||
Foreground(cBlue).
|
||||
Bold(true)
|
||||
|
||||
sToolOutput = lipgloss.NewStyle().
|
||||
Foreground(cGreen)
|
||||
|
||||
sToolResult = lipgloss.NewStyle().
|
||||
Foreground(cOverlay)
|
||||
|
||||
sSystem = lipgloss.NewStyle().
|
||||
Foreground(cYellow)
|
||||
|
||||
sError = lipgloss.NewStyle().
|
||||
Foreground(cRed)
|
||||
|
||||
sHint = lipgloss.NewStyle().
|
||||
Foreground(cOverlay)
|
||||
|
||||
sCursor = lipgloss.NewStyle().
|
||||
Foreground(cPurple)
|
||||
)
|
||||
|
||||
// Status bar
|
||||
var (
|
||||
sStatusBar = lipgloss.NewStyle().
|
||||
Foreground(cSubtext)
|
||||
|
||||
sStatusHighlight = lipgloss.NewStyle().
|
||||
Foreground(cPurple).
|
||||
Bold(true)
|
||||
|
||||
sStatusDim = lipgloss.NewStyle().
|
||||
Foreground(cOverlay)
|
||||
|
||||
sStatusStreaming = lipgloss.NewStyle().
|
||||
Foreground(cYellow).
|
||||
Bold(true)
|
||||
|
||||
sStatusBranch = lipgloss.NewStyle().
|
||||
Foreground(cGreen)
|
||||
|
||||
sLine = lipgloss.NewStyle().
|
||||
Foreground(cSurface)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user