on main help sign in
add initial message handling
8d874b8Kujtim Hoxha
internal/db/messages.sql.go+23−11
@@ -7,6 +7,7 @@ package db
7
7
8import (
8import (
9 "context"
9 "context"
10+ "database/sql"
10)
11)
11
12
12const createMessage = `-- name: CreateMessage :one
13const createMessage = `-- name: CreateMessage :one
@@ -15,19 +16,21 @@ INSERT INTO messages (
15 session_id,
16 session_id,
16 role,
17 role,
17 parts,
18 parts,
19+ model,
18 created_at,
20 created_at,
19 updated_at
21 updated_at
20) VALUES (
22) VALUES (
21− ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
23+ ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
22)
24)
23−RETURNING id, session_id, role, parts, created_at, updated_at
25+RETURNING id, session_id, role, parts, model, created_at, updated_at, finished_at
24`
26`
25
27
26type CreateMessageParams struct {
28type CreateMessageParams struct {
27− ID string `json:"id"`
29+ ID string `json:"id"`
28− SessionID string `json:"session_id"`
30+ SessionID string `json:"session_id"`
29− Role string `json:"role"`
31+ Role string `json:"role"`
30− Parts string `json:"parts"`
32+ Parts string `json:"parts"`
33+ Model sql.NullString `json:"model"`
31}
34}
32
35
33func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
36func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
@@ -36,6 +39,7 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (M
36 arg.SessionID,
39 arg.SessionID,
37 arg.Role,
40 arg.Role,
38 arg.Parts,
41 arg.Parts,
42+ arg.Model,
39 )
43 )
40 var i Message
44 var i Message
41 err := row.Scan(
45 err := row.Scan(
@@ -43,8 +47,10 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (M
43 &i.SessionID,
47 &i.SessionID,
44 &i.Role,
48 &i.Role,
45 &i.Parts,
49 &i.Parts,
50+ &i.Model,
46 &i.CreatedAt,
51 &i.CreatedAt,
47 &i.UpdatedAt,
52 &i.UpdatedAt,
53+ &i.FinishedAt,
48 )
54 )
49 return i, err
55 return i, err
50}
56}
@@ -70,7 +76,7 @@ func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) e
70}
76}
71
77
72const getMessage = `-- name: GetMessage :one
78const getMessage = `-- name: GetMessage :one
73−SELECT id, session_id, role, parts, created_at, updated_at
79+SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at
74FROM messages
80FROM messages
75WHERE id = ? LIMIT 1
81WHERE id = ? LIMIT 1
76`
82`
@@ -83,14 +89,16 @@ func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) {
83 &i.SessionID,
89 &i.SessionID,
84 &i.Role,
90 &i.Role,
85 &i.Parts,
91 &i.Parts,
92+ &i.Model,
86 &i.CreatedAt,
93 &i.CreatedAt,
87 &i.UpdatedAt,
94 &i.UpdatedAt,
95+ &i.FinishedAt,
88 )
96 )
89 return i, err
97 return i, err
90}
98}
91
99
92const listMessagesBySession = `-- name: ListMessagesBySession :many
100const listMessagesBySession = `-- name: ListMessagesBySession :many
93−SELECT id, session_id, role, parts, created_at, updated_at
101+SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at
94FROM messages
102FROM messages
95WHERE session_id = ?
103WHERE session_id = ?
96ORDER BY created_at ASC
104ORDER BY created_at ASC
@@ -110,8 +118,10 @@ func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) (
110 &i.SessionID,
118 &i.SessionID,
111 &i.Role,
119 &i.Role,
112 &i.Parts,
120 &i.Parts,
121+ &i.Model,
113 &i.CreatedAt,
122 &i.CreatedAt,
114 &i.UpdatedAt,
123 &i.UpdatedAt,
124+ &i.FinishedAt,
115 ); err != nil {
125 ); err != nil {
116 return nil, err
126 return nil, err
117 }
127 }
@@ -130,16 +140,18 @@ const updateMessage = `-- name: UpdateMessage :exec
130UPDATE messages
140UPDATE messages
131SET
141SET
132 parts = ?,
142 parts = ?,
143+ finished_at = ?,
133 updated_at = strftime('%s', 'now')
144 updated_at = strftime('%s', 'now')
134WHERE id = ?
145WHERE id = ?
135`
146`
136
147
137type UpdateMessageParams struct {
148type UpdateMessageParams struct {
138− Parts string `json:"parts"`
149+ Parts string `json:"parts"`
150+ FinishedAt sql.NullInt64 `json:"finished_at"`
139− ID string `json:"id"`
151+ ID string `json:"id"`
140}
152}
141
153
142func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error {
154func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error {
143− _, err := q.exec(ctx, q.updateMessageStmt, updateMessage, arg.Parts, arg.ID)
155+ _, err := q.exec(ctx, q.updateMessageStmt, updateMessage, arg.Parts, arg.FinishedAt, arg.ID)
144 return err
156 return err
145}
157}
internal/db/migrations/000001_initial.up.sql+2
@@ -24,8 +24,10 @@ CREATE TABLE IF NOT EXISTS messages (
24 session_id TEXT NOT NULL,
24 session_id TEXT NOT NULL,
25 role TEXT NOT NULL,
25 role TEXT NOT NULL,
26 parts TEXT NOT NULL default '[]',
26 parts TEXT NOT NULL default '[]',
27+ model TEXT,
27 created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
28 created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
28 updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
29 updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
30+ finished_at INTEGER, -- Unix timestamp in milliseconds
29 FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
31 FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
30);
32);
31
33
internal/db/models.go+8−6
@@ -9,12 +9,14 @@ import (
9)
9)
10
10
11type Message struct {
11type Message struct {
12− ID string `json:"id"`
12+ ID string `json:"id"`
13− SessionID string `json:"session_id"`
13+ SessionID string `json:"session_id"`
14− Role string `json:"role"`
14+ Role string `json:"role"`
15− Parts string `json:"parts"`
15+ Parts string `json:"parts"`
16+ Model sql.NullString `json:"model"`
16− CreatedAt int64 `json:"created_at"`
17+ CreatedAt int64 `json:"created_at"`
17− UpdatedAt int64 `json:"updated_at"`
18+ UpdatedAt int64 `json:"updated_at"`
19+ FinishedAt sql.NullInt64 `json:"finished_at"`
18}
20}
19
21
20type Session struct {
22type Session struct {
internal/db/sql/messages.sql+4−1
@@ -15,10 +15,11 @@ INSERT INTO messages (
15 session_id,
15 session_id,
16 role,
16 role,
17 parts,
17 parts,
18+ model,
18 created_at,
19 created_at,
19 updated_at
20 updated_at
20) VALUES (
21) VALUES (
21− ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
22+ ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
22)
23)
23RETURNING *;
24RETURNING *;
24
25
@@ -26,9 +27,11 @@ RETURNING *;
26UPDATE messages
27UPDATE messages
27SET
28SET
28 parts = ?,
29 parts = ?,
30+ finished_at = ?,
29 updated_at = strftime('%s', 'now')
31 updated_at = strftime('%s', 'now')
30WHERE id = ?;
32WHERE id = ?;
31
33
34+
32-- name: DeleteMessage :exec
35-- name: DeleteMessage :exec
33DELETE FROM messages
36DELETE FROM messages
34WHERE id = ?;
37WHERE id = ?;
internal/message/content.go+13−1
@@ -2,6 +2,7 @@ package message
2
2
3import (
3import (
4 "encoding/base64"
4 "encoding/base64"
5+ "time"
5)
6)
6
7
7type MessageRole string
8type MessageRole string
@@ -64,6 +65,7 @@ type ToolCall struct {
64 Name string `json:"name"`
65 Name string `json:"name"`
65 Input string `json:"input"`
66 Input string `json:"input"`
66 Type string `json:"type"`
67 Type string `json:"type"`
68+ Metadata any `json:"metadata"`
67 Finished bool `json:"finished"`
69 Finished bool `json:"finished"`
68}
70}
69
71
@@ -80,6 +82,7 @@ func (ToolResult) isPart() {}
80
82
81type Finish struct {
83type Finish struct {
82 Reason string `json:"reason"`
84 Reason string `json:"reason"`
85+ Time int64 `json:"time"`
83}
86}
84
87
85func (Finish) isPart() {}
88func (Finish) isPart() {}
@@ -161,6 +164,15 @@ func (m *Message) IsFinished() bool {
161 return false
164 return false
162}
165}
163
166
167+func (m *Message) FinishPart() *Finish {
168+ for _, part := range m.Parts {
169+ if c, ok := part.(Finish); ok {
170+ return &c
171+ }
172+ }
173+ return nil
174+}
175+
164func (m *Message) FinishReason() string {
176func (m *Message) FinishReason() string {
165 for _, part := range m.Parts {
177 for _, part := range m.Parts {
166 if c, ok := part.(Finish); ok {
178 if c, ok := part.(Finish); ok {
@@ -232,7 +244,7 @@ func (m *Message) SetToolResults(tr []ToolResult) {
232}
244}
233
245
234func (m *Message) AddFinish(reason string) {
246func (m *Message) AddFinish(reason string) {
235− m.Parts = append(m.Parts, Finish{Reason: reason})
247+ m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix()})
236}
248}
237
249
238func (m *Message) AddImageURL(url, detail string) {
250func (m *Message) AddImageURL(url, detail string) {
internal/message/message.go+12−2
@@ -2,17 +2,20 @@ package message
2
2
3import (
3import (
4 "context"
4 "context"
5+ "database/sql"
5 "encoding/json"
6 "encoding/json"
6 "fmt"
7 "fmt"
7
8
8 "github.com/google/uuid"
9 "github.com/google/uuid"
9 "github.com/kujtimiihoxha/termai/internal/db"
10 "github.com/kujtimiihoxha/termai/internal/db"
11+ "github.com/kujtimiihoxha/termai/internal/llm/models"
10 "github.com/kujtimiihoxha/termai/internal/pubsub"
12 "github.com/kujtimiihoxha/termai/internal/pubsub"
11)
13)
12
14
13type CreateMessageParams struct {
15type CreateMessageParams struct {
14 Role MessageRole
16 Role MessageRole
15 Parts []ContentPart
17 Parts []ContentPart
18+ Model models.ModelID
16}
19}
17
20
18type Service interface {
21type Service interface {
@@ -68,6 +71,7 @@ func (s *service) Create(sessionID string, params CreateMessageParams) (Message,
68 SessionID: sessionID,
71 SessionID: sessionID,
69 Role: string(params.Role),
72 Role: string(params.Role),
70 Parts: string(partsJSON),
73 Parts: string(partsJSON),
74+ Model: sql.NullString{String: string(params.Model), Valid: true},
71 })
75 })
72 if err != nil {
76 if err != nil {
73 return Message{}, err
77 return Message{}, err
@@ -101,9 +105,15 @@ func (s *service) Update(message Message) error {
101 if err != nil {
105 if err != nil {
102 return err
106 return err
103 }
107 }
108+ finishedAt := sql.NullInt64{}
109+ if f := message.FinishPart(); f != nil {
110+ finishedAt.Int64 = f.Time
111+ finishedAt.Valid = true
112+ }
104 err = s.q.UpdateMessage(s.ctx, db.UpdateMessageParams{
113 err = s.q.UpdateMessage(s.ctx, db.UpdateMessageParams{
105− ID: message.ID,
114+ ID: message.ID,
106− Parts: string(parts),
115+ Parts: string(parts),
116+ FinishedAt: finishedAt,
107 })
117 })
108 if err != nil {
118 if err != nil {
109 return err
119 return err
internal/tui/components/chat/chat.gonew file+113
@@ -0,0 +1,113 @@
1+package chat
2+
3+import (
4+ "fmt"
5+
6+ "github.com/charmbracelet/lipgloss"
7+ "github.com/charmbracelet/x/ansi"
8+ "github.com/kujtimiihoxha/termai/internal/config"
9+ "github.com/kujtimiihoxha/termai/internal/session"
10+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
11+ "github.com/kujtimiihoxha/termai/internal/version"
12+)
13+
14+type SendMsg struct {
15+ Text string
16+}
17+
18+type SessionSelectedMsg = session.Session
19+
20+type SessionClearedMsg struct{}
21+
22+type AgentWorkingMsg bool
23+
24+type EditorFocusMsg bool
25+
26+func lspsConfigured(width int) string {
27+ cfg := config.Get()
28+ title := "LSP Configuration"
29+ title = ansi.Truncate(title, width, "…")
30+
31+ lsps := styles.BaseStyle.Width(width).Foreground(styles.PrimaryColor).Bold(true).Render(title)
32+
33+ var lspViews []string
34+ for name, lsp := range cfg.LSP {
35+ lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
36+ fmt.Sprintf("• %s", name),
37+ )
38+ cmd := lsp.Command
39+ cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
40+ lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
41+ fmt.Sprintf(" (%s)", cmd),
42+ )
43+ lspViews = append(lspViews,
44+ styles.BaseStyle.
45+ Width(width).
46+ Render(
47+ lipgloss.JoinHorizontal(
48+ lipgloss.Left,
49+ lspName,
50+ lspPath,
51+ ),
52+ ),
53+ )
54+
55+ }
56+ return styles.BaseStyle.
57+ Width(width).
58+ Render(
59+ lipgloss.JoinVertical(
60+ lipgloss.Left,
61+ lsps,
62+ lipgloss.JoinVertical(
63+ lipgloss.Left,
64+ lspViews...,
65+ ),
66+ ),
67+ )
68+}
69+
70+func logo(width int) string {
71+ logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
72+
73+ version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
74+
75+ return styles.BaseStyle.
76+ Bold(true).
77+ Width(width).
78+ Render(
79+ lipgloss.JoinHorizontal(
80+ lipgloss.Left,
81+ logo,
82+ " ",
83+ version,
84+ ),
85+ )
86+}
87+
88+func repo(width int) string {
89+ repo := "https://github.com/kujtimiihoxha/opencode"
90+ return styles.BaseStyle.
91+ Foreground(styles.ForgroundDim).
92+ Width(width).
93+ Render(repo)
94+}
95+
96+func cwd(width int) string {
97+ cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
98+ return styles.BaseStyle.
99+ Foreground(styles.ForgroundDim).
100+ Width(width).
101+ Render(cwd)
102+}
103+
104+func header(width int) string {
105+ header := lipgloss.JoinVertical(
106+ lipgloss.Top,
107+ logo(width),
108+ repo(width),
109+ "",
110+ cwd(width),
111+ )
112+ return header
113+}
internal/tui/components/chat/editor.go+40−25
@@ -7,10 +7,12 @@ import (
7 "github.com/charmbracelet/lipgloss"
7 "github.com/charmbracelet/lipgloss"
8 "github.com/kujtimiihoxha/termai/internal/tui/layout"
8 "github.com/kujtimiihoxha/termai/internal/tui/layout"
9 "github.com/kujtimiihoxha/termai/internal/tui/styles"
9 "github.com/kujtimiihoxha/termai/internal/tui/styles"
10+ "github.com/kujtimiihoxha/termai/internal/tui/util"
10)
11)
11
12
12type editorCmp struct {
13type editorCmp struct {
13− textarea textarea.Model
14+ textarea textarea.Model
15+ agentWorking bool
14}
16}
15
17
16type focusedEditorKeyMaps struct {
18type focusedEditorKeyMaps struct {
@@ -49,39 +51,51 @@ func (m *editorCmp) Init() tea.Cmd {
49 return textarea.Blink
51 return textarea.Blink
50}
52}
51
53
54+func (m *editorCmp) send() tea.Cmd {
55+ if m.agentWorking {
56+ return util.ReportWarn("Agent is working, please wait...")
57+ }
58+
59+ value := m.textarea.Value()
60+ m.textarea.Reset()
61+ m.textarea.Blur()
62+ if value == "" {
63+ return nil
64+ }
65+ return tea.Batch(
66+ util.CmdHandler(SendMsg{
67+ Text: value,
68+ }),
69+ util.CmdHandler(AgentWorkingMsg(true)),
70+ util.CmdHandler(EditorFocusMsg(false)),
71+ )
72+}
73+
52func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
74func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
53 var cmd tea.Cmd
75 var cmd tea.Cmd
54− if m.textarea.Focused() {
55− switch msg := msg.(type) {
56− case tea.KeyMsg:
57− if key.Matches(msg, focusedKeyMaps.Send) {
58− // TODO: send message
59− m.textarea.Reset()
60− m.textarea.Blur()
61− return m, nil
62− }
63− if key.Matches(msg, focusedKeyMaps.Blur) {
64− m.textarea.Blur()
65− return m, nil
66− }
67− }
68− m.textarea, cmd = m.textarea.Update(msg)
69− return m, cmd
70− }
71 switch msg := msg.(type) {
76 switch msg := msg.(type) {
77+ case AgentWorkingMsg:
78+ m.agentWorking = bool(msg)
72 case tea.KeyMsg:
79 case tea.KeyMsg:
80+ if key.Matches(msg, focusedKeyMaps.Send) {
81+ return m, m.send()
82+ }
73 if key.Matches(msg, bluredKeyMaps.Send) {
83 if key.Matches(msg, bluredKeyMaps.Send) {
74− // TODO: send message
84+ return m, m.send()
85+ }
86+ if key.Matches(msg, focusedKeyMaps.Blur) {
75− m.textarea.Reset()
87+ m.textarea.Blur()
76− return m, nil
88+ return m, util.CmdHandler(EditorFocusMsg(false))
77 }
89 }
78 if key.Matches(msg, bluredKeyMaps.Focus) {
90 if key.Matches(msg, bluredKeyMaps.Focus) {
91+ if !m.textarea.Focused() {
79− m.textarea.Focus()
92+ m.textarea.Focus()
80− return m, textarea.Blink
93+ return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
94+ }
81 }
95 }
82 }
96 }
83−
97+ m.textarea, cmd = m.textarea.Update(msg)
84− return m, nil
98+ return m, cmd
85}
99}
86
100
87func (m *editorCmp) View() string {
101func (m *editorCmp) View() string {
@@ -122,6 +136,7 @@ func NewEditorCmp() tea.Model {
122 ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
136 ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
123 ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
137 ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
124 ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
138 ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
139+ ti.CharLimit = -1
125 ti.Focus()
140 ti.Focus()
126 return &editorCmp{
141 return &editorCmp{
127 textarea: ti,
142 textarea: ti,
internal/tui/components/chat/messages.go+331−8
@@ -1,21 +1,344 @@
1package chat
1package chat
2
2
3−import tea "github.com/charmbracelet/bubbletea"
3+import (
4+ "fmt"
5+ "regexp"
6+ "strconv"
7+ "strings"
4
8
9+ "github.com/charmbracelet/bubbles/viewport"
10+ tea "github.com/charmbracelet/bubbletea"
11+ "github.com/charmbracelet/glamour"
12+ "github.com/charmbracelet/lipgloss"
13+ "github.com/kujtimiihoxha/termai/internal/app"
14+ "github.com/kujtimiihoxha/termai/internal/message"
15+ "github.com/kujtimiihoxha/termai/internal/pubsub"
16+ "github.com/kujtimiihoxha/termai/internal/session"
17+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
18+ "github.com/kujtimiihoxha/termai/internal/tui/util"
19+)
20+
21+type uiMessage struct {
22+ position int
23+ height int
24+ content string
25+}
26+
5−type messagesCmp struct{}
27+type messagesCmp struct {
28+ app *app.App
29+ width, height int
30+ writingMode bool
31+ viewport viewport.Model
32+ session session.Session
33+ messages []message.Message
34+ uiMessages []uiMessage
35+ currentIndex int
36+ renderer *glamour.TermRenderer
37+ focusRenderer *glamour.TermRenderer
38+ cachedContent map[string]string
39+}
6
40
7func (m *messagesCmp) Init() tea.Cmd {
41func (m *messagesCmp) Init() tea.Cmd {
8− return nil
42+ return m.viewport.Init()
43+}
44+
45+var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
46+
47+func hexToBgSGR(hex string) (string, error) {
48+ hex = strings.TrimPrefix(hex, "#")
49+ if len(hex) != 6 {
50+ return "", fmt.Errorf("invalid hex color: must be 6 hexadecimal digits")
51+ }
52+
53+ // Parse RGB components in one block
54+ rgb := make([]uint64, 3)
55+ for i := 0; i < 3; i++ {
56+ val, err := strconv.ParseUint(hex[i*2:i*2+2], 16, 8)
57+ if err != nil {
58+ return "", err
59+ }
60+ rgb[i] = val
61+ }
62+
63+ return fmt.Sprintf("48;2;%d;%d;%d", rgb[0], rgb[1], rgb[2]), nil
64+}
65+
66+func forceReplaceBackgroundColors(input string, newBg string) string {
67+ return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string {
68+ // Extract content between "\x1b[" and "m"
69+ content := seq[2 : len(seq)-1]
70+ tokens := strings.Split(content, ";")
71+ var newTokens []string
72+
73+ // Skip background color tokens
74+ for i := 0; i < len(tokens); i++ {
75+ if tokens[i] == "" {
76+ continue
77+ }
78+
79+ val, err := strconv.Atoi(tokens[i])
80+ if err != nil {
81+ newTokens = append(newTokens, tokens[i])
82+ continue
83+ }
84+
85+ // Skip background color tokens
86+ if val == 48 {
87+ // Skip "48;5;N" or "48;2;R;G;B" sequences
88+ if i+1 < len(tokens) {
89+ if nextVal, err := strconv.Atoi(tokens[i+1]); err == nil {
90+ switch nextVal {
91+ case 5:
92+ i += 2 // Skip "5" and color index
93+ case 2:
94+ i += 4 // Skip "2" and RGB components
95+ }
96+ }
97+ }
98+ } else if (val < 40 || val > 47) && (val < 100 || val > 107) && val != 49 {
99+ // Keep non-background tokens
100+ newTokens = append(newTokens, tokens[i])
101+ }
102+ }
103+
104+ // Add new background if provided
105+ if newBg != "" {
106+ newTokens = append(newTokens, strings.Split(newBg, ";")...)
107+ }
108+
109+ if len(newTokens) == 0 {
110+ return ""
111+ }
112+
113+ return "\x1b[" + strings.Join(newTokens, ";") + "m"
114+ })
115+}
116+
117+func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
118+ switch msg := msg.(type) {
119+ case EditorFocusMsg:
120+ m.writingMode = bool(msg)
121+ case SessionSelectedMsg:
122+ if msg.ID != m.session.ID {
123+ cmd := m.SetSession(msg)
124+ return m, cmd
125+ }
126+ return m, nil
127+ case pubsub.Event[message.Message]:
128+ if msg.Type == pubsub.CreatedEvent {
129+ if msg.Payload.SessionID == m.session.ID {
130+ // check if message exists
131+ for _, v := range m.messages {
132+ if v.ID == msg.Payload.ID {
133+ return m, nil
134+ }
135+ }
136+
137+ m.messages = append(m.messages, msg.Payload)
138+ m.renderView()
139+ m.viewport.GotoBottom()
140+ }
141+ for _, v := range m.messages {
142+ for _, c := range v.ToolCalls() {
143+ // the message is being added to the session of a tool called
144+ if c.ID == msg.Payload.SessionID {
145+ m.renderView()
146+ m.viewport.GotoBottom()
147+ }
148+ }
149+ }
150+ } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
151+ for i, v := range m.messages {
152+ if v.ID == msg.Payload.ID {
153+ m.messages[i] = msg.Payload
154+ delete(m.cachedContent, msg.Payload.ID)
155+ m.renderView()
156+ if i == len(m.messages)-1 {
157+ m.viewport.GotoBottom()
158+ }
159+ break
160+ }
161+ }
162+ }
163+ }
164+ u, cmd := m.viewport.Update(msg)
165+ m.viewport = u
166+ return m, cmd
9}
167}
10
168
11−func (m *messagesCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
169+func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string {
170+ if v, ok := m.cachedContent[msg.ID]; ok {
171+ return v
172+ }
173+ style := styles.BaseStyle.
174+ Width(m.width).
175+ BorderLeft(true).
176+ Foreground(styles.ForgroundDim).
177+ BorderForeground(styles.ForgroundDim).
178+ BorderStyle(lipgloss.ThickBorder())
179+
180+ renderer := m.renderer
181+ if inx == m.currentIndex {
182+ style = style.
183+ Foreground(styles.Forground).
184+ BorderForeground(styles.Blue).
185+ BorderStyle(lipgloss.ThickBorder())
186+ renderer = m.focusRenderer
187+ }
188+ c, _ := renderer.Render(msg.Content().String())
189+ col, _ := hexToBgSGR(styles.Background.Dark)
190+ rendered := style.Render(forceReplaceBackgroundColors(c, col))
191+ m.cachedContent[msg.ID] = rendered
12− return m, nil
192+ return rendered
193+}
194+
195+func (m *messagesCmp) renderView() {
196+ m.uiMessages = make([]uiMessage, 0)
197+ pos := 0
198+
199+ for _, v := range m.messages {
200+ content := ""
201+ switch v.Role {
202+ case message.User:
203+ content = m.renderUserMessage(pos, v)
204+ }
205+ m.uiMessages = append(m.uiMessages, uiMessage{
206+ position: pos,
207+ height: lipgloss.Height(content),
208+ content: content,
209+ })
210+ pos += lipgloss.Height(content) + 1 // + 1 for spacing
211+ }
212+
213+ messages := make([]string, 0)
214+ for _, v := range m.uiMessages {
215+ messages = append(messages, v.content)
216+ }
217+ m.viewport.SetContent(
218+ styles.BaseStyle.
219+ Width(m.width).
220+ Render(
221+ lipgloss.JoinVertical(
222+ lipgloss.Top,
223+ messages...,
224+ ),
225+ ),
226+ )
13}
227}
14
228
15func (m *messagesCmp) View() string {
229func (m *messagesCmp) View() string {
16− return "Messages"
230+ if len(m.messages) == 0 {
231+ content := styles.BaseStyle.
232+ Width(m.width).
233+ Height(m.height - 1).
234+ Render(
235+ m.initialScreen(),
236+ )
237+
238+ return styles.BaseStyle.
239+ Width(m.width).
240+ Render(
241+ lipgloss.JoinVertical(
242+ lipgloss.Top,
243+ content,
244+ m.help(),
245+ ),
246+ )
247+ }
248+
249+ m.renderView()
250+ return styles.BaseStyle.
251+ Width(m.width).
252+ Render(
253+ lipgloss.JoinVertical(
254+ lipgloss.Top,
255+ m.viewport.View(),
256+ m.help(),
257+ ),
258+ )
259+}
260+
261+func (m *messagesCmp) help() string {
262+ text := ""
263+ if m.writingMode {
264+ text = lipgloss.JoinHorizontal(
265+ lipgloss.Left,
266+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
267+ styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
268+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
269+ )
270+ } else {
271+ text = lipgloss.JoinHorizontal(
272+ lipgloss.Left,
273+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
274+ styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
275+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to start writing"),
276+ )
277+ }
278+
279+ return styles.BaseStyle.
280+ Width(m.width).
281+ Render(text)
282+}
283+
284+func (m *messagesCmp) initialScreen() string {
285+ return styles.BaseStyle.Width(m.width).Render(
286+ lipgloss.JoinVertical(
287+ lipgloss.Top,
288+ header(m.width),
289+ "",
290+ lspsConfigured(m.width),
291+ ),
292+ )
293+}
294+
295+func (m *messagesCmp) SetSize(width, height int) {
296+ m.width = width
297+ m.height = height
298+ m.viewport.Width = width
299+ m.viewport.Height = height - 1
300+ focusRenderer, _ := glamour.NewTermRenderer(
301+ glamour.WithStyles(styles.MarkdownTheme(true)),
302+ glamour.WithWordWrap(width-1),
303+ )
304+ renderer, _ := glamour.NewTermRenderer(
305+ glamour.WithStyles(styles.MarkdownTheme(false)),
306+ glamour.WithWordWrap(width-1),
307+ )
308+ m.focusRenderer = focusRenderer
309+ m.renderer = renderer
310+}
311+
312+func (m *messagesCmp) GetSize() (int, int) {
313+ return m.width, m.height
314+}
315+
316+func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
317+ m.session = session
318+ messages, err := m.app.Messages.List(session.ID)
319+ if err != nil {
320+ return util.ReportError(err)
321+ }
322+ m.messages = messages
323+ m.messages = append(m.messages, m.messages[0])
324+ return nil
17}
325}
18
326
19−func NewMessagesCmp() tea.Model {
327+func NewMessagesCmp(app *app.App) tea.Model {
328+ focusRenderer, _ := glamour.NewTermRenderer(
329+ glamour.WithStyles(styles.MarkdownTheme(true)),
330+ glamour.WithWordWrap(80),
331+ )
332+ renderer, _ := glamour.NewTermRenderer(
333+ glamour.WithStyles(styles.MarkdownTheme(false)),
334+ glamour.WithWordWrap(80),
335+ )
20− return &messagesCmp{}
336+ return &messagesCmp{
337+ app: app,
338+ writingMode: true,
339+ cachedContent: make(map[string]string),
340+ viewport: viewport.New(0, 0),
341+ focusRenderer: focusRenderer,
342+ renderer: renderer,
343+ }
21}
344}
internal/tui/components/chat/sidebar.go+28−105
@@ -5,40 +5,43 @@ import (
5
5
6 tea "github.com/charmbracelet/bubbletea"
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/charmbracelet/lipgloss"
7 "github.com/charmbracelet/lipgloss"
8− "github.com/kujtimiihoxha/termai/internal/config"
8+ "github.com/kujtimiihoxha/termai/internal/session"
9 "github.com/kujtimiihoxha/termai/internal/tui/styles"
9 "github.com/kujtimiihoxha/termai/internal/tui/styles"
10− "github.com/kujtimiihoxha/termai/internal/version"
11)
10)
12
11
13type sidebarCmp struct {
12type sidebarCmp struct {
14 width, height int
13 width, height int
14+ session session.Session
15}
15}
16
16
17func (m *sidebarCmp) Init() tea.Cmd {
17func (m *sidebarCmp) Init() tea.Cmd {
18 return nil
18 return nil
19}
19}
20
20
21−func (m *sidebarCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
21+func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
22 return m, nil
22 return m, nil
23}
23}
24
24
25func (m *sidebarCmp) View() string {
25func (m *sidebarCmp) View() string {
26− return styles.BaseStyle.Width(m.width).Render(
26+ return styles.BaseStyle.
27+ Width(m.width).
28+ Height(m.height - 1).
29+ Render(
27− lipgloss.JoinVertical(
30+ lipgloss.JoinVertical(
28− lipgloss.Top,
31+ lipgloss.Top,
29− m.header(),
32+ header(m.width),
30− " ",
33+ " ",
31− m.session(),
34+ m.sessionSection(),
32− " ",
35+ " ",
33− m.modifiedFiles(),
36+ m.modifiedFiles(),
34− " ",
37+ " ",
35− m.lspsConfigured(),
38+ lspsConfigured(m.width),
36− ),
39+ ),
37− )
40+ )
38}
41}
39
42
40−func (m *sidebarCmp) session() string {
43+func (m *sidebarCmp) sessionSection() string {
41− sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Render("Session")
44+ sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session")
42 sessionValue := styles.BaseStyle.
45 sessionValue := styles.BaseStyle.
43 Foreground(styles.Forground).
46 Foreground(styles.Forground).
44 Width(m.width - lipgloss.Width(sessionKey)).
47 Width(m.width - lipgloss.Width(sessionKey)).
@@ -53,11 +56,11 @@ func (m *sidebarCmp) session() string {
53func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
56func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
54 stats := ""
57 stats := ""
55 if additions > 0 && removals > 0 {
58 if additions > 0 && removals > 0 {
56− stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%d additions and %d removals", additions, removals))
59+ stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d additions and %d removals", additions, removals))
57 } else if additions > 0 {
60 } else if additions > 0 {
58− stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%d additions", additions))
61+ stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d additions", additions))
59 } else if removals > 0 {
62 } else if removals > 0 {
60− stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%d removals", removals))
63+ stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d removals", removals))
61 }
64 }
62 filePathStr := styles.BaseStyle.Foreground(styles.Forground).Render(filePath)
65 filePathStr := styles.BaseStyle.Foreground(styles.Forground).Render(filePath)
63
66
@@ -67,60 +70,13 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri
67 lipgloss.JoinHorizontal(
70 lipgloss.JoinHorizontal(
68 lipgloss.Left,
71 lipgloss.Left,
69 filePathStr,
72 filePathStr,
70− " ",
71 stats,
73 stats,
72 ),
74 ),
73 )
75 )
74}
76}
75
77
76−func (m *sidebarCmp) lspsConfigured() string {
77− lsps := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Render("LSP Configuration:")
78− lspsConfigured := []struct {
79− name string
80− path string
81− }{
82− {"golsp", "path/to/lsp1"},
83− {"vtsls", "path/to/lsp2"},
84− }
85−
86− var lspViews []string
87− for _, lsp := range lspsConfigured {
88− lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
89− fmt.Sprintf("• %s", lsp.name),
90− )
91− lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
92− fmt.Sprintf("(%s)", lsp.path),
93− )
94− lspViews = append(lspViews,
95− styles.BaseStyle.
96− Width(m.width).
97− Render(
98− lipgloss.JoinHorizontal(
99− lipgloss.Left,
100− lspName,
101− " ",
102− lspPath,
103− ),
104− ),
105− )
106−
107− }
108− return styles.BaseStyle.
109− Width(m.width).
110− Render(
111− lipgloss.JoinVertical(
112− lipgloss.Left,
113− lsps,
114− lipgloss.JoinVertical(
115− lipgloss.Left,
116− lspViews...,
117− ),
118− ),
119− )
120−}
121−
122func (m *sidebarCmp) modifiedFiles() string {
78func (m *sidebarCmp) modifiedFiles() string {
123− modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Render("Modified Files:")
79+ modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render("Modified Files:")
124 files := []struct {
80 files := []struct {
125 path string
81 path string
126 additions int
82 additions int
@@ -149,41 +105,6 @@ func (m *sidebarCmp) modifiedFiles() string {
149 )
105 )
150}
106}
151
107
152−func (m *sidebarCmp) logo() string {
153− logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
154−
155− version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
156−
157− return styles.BaseStyle.
158− Bold(true).
159− Width(m.width).
160− Render(
161− lipgloss.JoinHorizontal(
162− lipgloss.Left,
163− logo,
164− " ",
165− version,
166− ),
167− )
168−}
169−
170−func (m *sidebarCmp) header() string {
171− header := lipgloss.JoinVertical(
172− lipgloss.Top,
173− m.logo(),
174− m.cwd(),
175− )
176− return header
177−}
178−
179−func (m *sidebarCmp) cwd() string {
180− cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
181− return styles.BaseStyle.
182− Foreground(styles.ForgroundDim).
183− Width(m.width).
184− Render(cwd)
185−}
186−
187func (m *sidebarCmp) SetSize(width, height int) {
108func (m *sidebarCmp) SetSize(width, height int) {
188 m.width = width
109 m.width = width
189 m.height = height
110 m.height = height
@@ -193,6 +114,8 @@ func (m *sidebarCmp) GetSize() (int, int) {
193 return m.width, m.height
114 return m.width, m.height
194}
115}
195
116
196−func NewSidebarCmp() tea.Model {
117+func NewSidebarCmp(session session.Session) tea.Model {
197− return &sidebarCmp{}
118+ return &sidebarCmp{
119+ session: session,
120+ }
198}
121}
internal/tui/components/core/button.godeleted−287
@@ -1,287 +0,0 @@
1−package core
2−
3−import (
4− "github.com/charmbracelet/bubbles/key"
5− tea "github.com/charmbracelet/bubbletea"
6− "github.com/charmbracelet/lipgloss"
7− "github.com/kujtimiihoxha/termai/internal/tui/styles"
8−)
9−
10−// ButtonKeyMap defines key bindings for the button component
11−type ButtonKeyMap struct {
12− Enter key.Binding
13−}
14−
15−// DefaultButtonKeyMap returns default key bindings for the button
16−func DefaultButtonKeyMap() ButtonKeyMap {
17− return ButtonKeyMap{
18− Enter: key.NewBinding(
19− key.WithKeys("enter"),
20− key.WithHelp("enter", "select"),
21− ),
22− }
23−}
24−
25−// ShortHelp returns keybinding help
26−func (k ButtonKeyMap) ShortHelp() []key.Binding {
27− return []key.Binding{k.Enter}
28−}
29−
30−// FullHelp returns full help info for keybindings
31−func (k ButtonKeyMap) FullHelp() [][]key.Binding {
32− return [][]key.Binding{
33− {k.Enter},
34− }
35−}
36−
37−// ButtonState represents the state of a button
38−type ButtonState int
39−
40−const (
41− // ButtonNormal is the default state
42− ButtonNormal ButtonState = iota
43− // ButtonHovered is when the button is focused/hovered
44− ButtonHovered
45− // ButtonPressed is when the button is being pressed
46− ButtonPressed
47− // ButtonDisabled is when the button is disabled
48− ButtonDisabled
49−)
50−
51−// ButtonVariant defines the visual style variant of a button
52−type ButtonVariant int
53−
54−const (
55− // ButtonPrimary uses primary color styling
56− ButtonPrimary ButtonVariant = iota
57− // ButtonSecondary uses secondary color styling
58− ButtonSecondary
59− // ButtonDanger uses danger/error color styling
60− ButtonDanger
61− // ButtonWarning uses warning color styling
62− ButtonWarning
63− // ButtonNeutral uses neutral color styling
64− ButtonNeutral
65−)
66−
67−// ButtonMsg is sent when a button is clicked
68−type ButtonMsg struct {
69− ID string
70− Payload any
71−}
72−
73−// ButtonCmp represents a clickable button component
74−type ButtonCmp struct {
75− id string
76− label string
77− width int
78− height int
79− state ButtonState
80− variant ButtonVariant
81− keyMap ButtonKeyMap
82− payload any
83− style lipgloss.Style
84− hoverStyle lipgloss.Style
85−}
86−
87−// NewButtonCmp creates a new button component
88−func NewButtonCmp(id, label string) *ButtonCmp {
89− b := &ButtonCmp{
90− id: id,
91− label: label,
92− state: ButtonNormal,
93− variant: ButtonPrimary,
94− keyMap: DefaultButtonKeyMap(),
95− width: len(label) + 4, // add some padding
96− height: 1,
97− }
98− b.updateStyles()
99− return b
100−}
101−
102−// WithVariant sets the button variant
103−func (b *ButtonCmp) WithVariant(variant ButtonVariant) *ButtonCmp {
104− b.variant = variant
105− b.updateStyles()
106− return b
107−}
108−
109−// WithPayload sets the payload sent with button events
110−func (b *ButtonCmp) WithPayload(payload any) *ButtonCmp {
111− b.payload = payload
112− return b
113−}
114−
115−// WithWidth sets a custom width
116−func (b *ButtonCmp) WithWidth(width int) *ButtonCmp {
117− b.width = width
118− b.updateStyles()
119− return b
120−}
121−
122−// updateStyles recalculates styles based on current state and variant
123−func (b *ButtonCmp) updateStyles() {
124− // Base styles
125− b.style = styles.Regular.
126− Padding(0, 1).
127− Width(b.width).
128− Align(lipgloss.Center).
129− BorderStyle(lipgloss.RoundedBorder())
130−
131− b.hoverStyle = b.style.
132− Bold(true)
133−
134− // Variant-specific styling
135− switch b.variant {
136− case ButtonPrimary:
137− b.style = b.style.
138− Foreground(styles.Base).
139− Background(styles.Primary).
140− BorderForeground(styles.Primary)
141−
142− b.hoverStyle = b.hoverStyle.
143− Foreground(styles.Base).
144− Background(styles.Blue).
145− BorderForeground(styles.Blue)
146−
147− case ButtonSecondary:
148− b.style = b.style.
149− Foreground(styles.Base).
150− Background(styles.Secondary).
151− BorderForeground(styles.Secondary)
152−
153− b.hoverStyle = b.hoverStyle.
154− Foreground(styles.Base).
155− Background(styles.Mauve).
156− BorderForeground(styles.Mauve)
157−
158− case ButtonDanger:
159− b.style = b.style.
160− Foreground(styles.Base).
161− Background(styles.Error).
162− BorderForeground(styles.Error)
163−
164− b.hoverStyle = b.hoverStyle.
165− Foreground(styles.Base).
166− Background(styles.Red).
167− BorderForeground(styles.Red)
168−
169− case ButtonWarning:
170− b.style = b.style.
171− Foreground(styles.Text).
172− Background(styles.Warning).
173− BorderForeground(styles.Warning)
174−
175− b.hoverStyle = b.hoverStyle.
176− Foreground(styles.Text).
177− Background(styles.Peach).
178− BorderForeground(styles.Peach)
179−
180− case ButtonNeutral:
181− b.style = b.style.
182− Foreground(styles.Text).
183− Background(styles.Grey).
184− BorderForeground(styles.Grey)
185−
186− b.hoverStyle = b.hoverStyle.
187− Foreground(styles.Text).
188− Background(styles.DarkGrey).
189− BorderForeground(styles.DarkGrey)
190− }
191−
192− // Disabled style override
193− if b.state == ButtonDisabled {
194− b.style = b.style.
195− Foreground(styles.SubText0).
196− Background(styles.LightGrey).
197− BorderForeground(styles.LightGrey)
198− }
199−}
200−
201−// SetSize sets the button size
202−func (b *ButtonCmp) SetSize(width, height int) {
203− b.width = width
204− b.height = height
205− b.updateStyles()
206−}
207−
208−// Focus sets the button to focused state
209−func (b *ButtonCmp) Focus() tea.Cmd {
210− if b.state != ButtonDisabled {
211− b.state = ButtonHovered
212− }
213− return nil
214−}
215−
216−// Blur sets the button to normal state
217−func (b *ButtonCmp) Blur() tea.Cmd {
218− if b.state != ButtonDisabled {
219− b.state = ButtonNormal
220− }
221− return nil
222−}
223−
224−// Disable sets the button to disabled state
225−func (b *ButtonCmp) Disable() {
226− b.state = ButtonDisabled
227− b.updateStyles()
228−}
229−
230−// Enable enables the button if disabled
231−func (b *ButtonCmp) Enable() {
232− if b.state == ButtonDisabled {
233− b.state = ButtonNormal
234− b.updateStyles()
235− }
236−}
237−
238−// IsDisabled returns whether the button is disabled
239−func (b *ButtonCmp) IsDisabled() bool {
240− return b.state == ButtonDisabled
241−}
242−
243−// IsFocused returns whether the button is focused
244−func (b *ButtonCmp) IsFocused() bool {
245− return b.state == ButtonHovered
246−}
247−
248−// Init initializes the button
249−func (b *ButtonCmp) Init() tea.Cmd {
250− return nil
251−}
252−
253−// Update handles messages and user input
254−func (b *ButtonCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
255− // Skip updates if disabled
256− if b.state == ButtonDisabled {
257− return b, nil
258− }
259−
260− switch msg := msg.(type) {
261− case tea.KeyMsg:
262− // Handle key presses when focused
263− if b.state == ButtonHovered {
264− switch {
265− case key.Matches(msg, b.keyMap.Enter):
266− b.state = ButtonPressed
267− return b, func() tea.Msg {
268− return ButtonMsg{
269− ID: b.id,
270− Payload: b.payload,
271− }
272− }
273− }
274− }
275− }
276−
277− return b, nil
278−}
279−
280−// View renders the button
281−func (b *ButtonCmp) View() string {
282− if b.state == ButtonHovered || b.state == ButtonPressed {
283− return b.hoverStyle.Render(b.label)
284− }
285− return b.style.Render(b.label)
286−}
287−
internal/tui/layout/split.go+24
@@ -10,6 +10,9 @@ import (
10type SplitPaneLayout interface {
10type SplitPaneLayout interface {
11 tea.Model
11 tea.Model
12 Sizeable
12 Sizeable
13+ SetLeftPanel(panel Container)
14+ SetRightPanel(panel Container)
15+ SetBottomPanel(panel Container)
13}
16}
14
17
15type splitPaneLayout struct {
18type splitPaneLayout struct {
@@ -160,6 +163,27 @@ func (s *splitPaneLayout) GetSize() (int, int) {
160 return s.width, s.height
163 return s.width, s.height
161}
164}
162
165
166+func (s *splitPaneLayout) SetLeftPanel(panel Container) {
167+ s.leftPanel = panel
168+ if s.width > 0 && s.height > 0 {
169+ s.SetSize(s.width, s.height)
170+ }
171+}
172+
173+func (s *splitPaneLayout) SetRightPanel(panel Container) {
174+ s.rightPanel = panel
175+ if s.width > 0 && s.height > 0 {
176+ s.SetSize(s.width, s.height)
177+ }
178+}
179+
180+func (s *splitPaneLayout) SetBottomPanel(panel Container) {
181+ s.bottomPanel = panel
182+ if s.width > 0 && s.height > 0 {
183+ s.SetSize(s.width, s.height)
184+ }
185+}
186+
163func (s *splitPaneLayout) BindingKeys() []key.Binding {
187func (s *splitPaneLayout) BindingKeys() []key.Binding {
164 keys := []key.Binding{}
188 keys := []key.Binding{}
165 if s.leftPanel != nil {
189 if s.leftPanel != nil {
internal/tui/page/chat.go+82−10
@@ -3,28 +3,100 @@ package page
3import (
3import (
4 tea "github.com/charmbracelet/bubbletea"
4 tea "github.com/charmbracelet/bubbletea"
5 "github.com/kujtimiihoxha/termai/internal/app"
5 "github.com/kujtimiihoxha/termai/internal/app"
6+ "github.com/kujtimiihoxha/termai/internal/message"
7+ "github.com/kujtimiihoxha/termai/internal/session"
6 "github.com/kujtimiihoxha/termai/internal/tui/components/chat"
8 "github.com/kujtimiihoxha/termai/internal/tui/components/chat"
7 "github.com/kujtimiihoxha/termai/internal/tui/layout"
9 "github.com/kujtimiihoxha/termai/internal/tui/layout"
10+ "github.com/kujtimiihoxha/termai/internal/tui/util"
8)
11)
9
12
10var ChatPage PageID = "chat"
13var ChatPage PageID = "chat"
11
14
12−func NewChatPage(app *app.App) tea.Model {
15+type chatPage struct {
16+ app *app.App
17+ layout layout.SplitPaneLayout
18+ session session.Session
19+}
20+
21+func (p *chatPage) Init() tea.Cmd {
22+ return p.layout.Init()
23+}
24+
25+func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
26+ switch msg := msg.(type) {
27+ case tea.WindowSizeMsg:
28+ p.layout.SetSize(msg.Width, msg.Height)
29+ case chat.SendMsg:
30+ cmd := p.sendMessage(msg.Text)
31+ if cmd != nil {
32+ return p, cmd
33+ }
34+ }
35+ u, cmd := p.layout.Update(msg)
36+ p.layout = u.(layout.SplitPaneLayout)
37+ if cmd != nil {
38+ return p, cmd
39+ }
40+ return p, nil
41+}
42+
43+func (p *chatPage) setSidebar() tea.Cmd {
13− messagesContainer := layout.NewContainer(
44+ sidebarContainer := layout.NewContainer(
14− chat.NewMessagesCmp(),
45+ chat.NewSidebarCmp(p.session),
15 layout.WithPadding(1, 1, 1, 1),
46 layout.WithPadding(1, 1, 1, 1),
16 )
47 )
48+ p.layout.SetRightPanel(sidebarContainer)
49+ width, height := p.layout.GetSize()
50+ p.layout.SetSize(width, height)
17− sidebarContainer := layout.NewContainer(
51+ return sidebarContainer.Init()
18− chat.NewSidebarCmp(),
52+}
53+
54+func (p *chatPage) sendMessage(text string) tea.Cmd {
55+ var cmds []tea.Cmd
56+ if p.session.ID == "" {
57+ session, err := p.app.Sessions.Create("New Session")
58+ if err != nil {
59+ return util.ReportError(err)
60+ }
61+
62+ p.session = session
63+ cmd := p.setSidebar()
64+ if cmd != nil {
65+ cmds = append(cmds, cmd)
66+ }
67+ cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
68+ }
69+ // TODO: actually call agent
70+ p.app.Messages.Create(p.session.ID, message.CreateMessageParams{
71+ Role: message.User,
72+ Parts: []message.ContentPart{
73+ message.TextContent{
74+ Text: text,
75+ },
76+ },
77+ })
78+ return tea.Batch(cmds...)
79+}
80+
81+func (p *chatPage) View() string {
82+ return p.layout.View()
83+}
84+
85+func NewChatPage(app *app.App) tea.Model {
86+ messagesContainer := layout.NewContainer(
87+ chat.NewMessagesCmp(app),
19 layout.WithPadding(1, 1, 1, 1),
88 layout.WithPadding(1, 1, 1, 1),
20 )
89 )
90+
21 editorContainer := layout.NewContainer(
91 editorContainer := layout.NewContainer(
22 chat.NewEditorCmp(),
92 chat.NewEditorCmp(),
23 layout.WithBorder(true, false, false, false),
93 layout.WithBorder(true, false, false, false),
24 )
94 )
95+ return &chatPage{
96+ app: app,
25− return layout.NewSplitPane(
97+ layout: layout.NewSplitPane(
26− layout.WithRightPanel(sidebarContainer),
27− layout.WithLeftPanel(messagesContainer),
98+ layout.WithLeftPanel(messagesContainer),
28− layout.WithBottomPanel(editorContainer),
99+ layout.WithBottomPanel(editorContainer),
29− )
100+ ),
101+ }
30}
102}
internal/tui/styles/markdown.go+444−2
@@ -36,12 +36,13 @@ var catppuccinDark = ansi.StyleConfig{
36 Italic: boolPtr(true),
36 Italic: boolPtr(true),
37 Prefix: "┃ ",
37 Prefix: "┃ ",
38 },
38 },
39− Indent: uintPtr(1),
39+ Indent: uintPtr(1),
40− Margin: uintPtr(defaultMargin),
40+ IndentToken: stringPtr(BaseStyle.Render(" ")),
41 },
41 },
42 List: ansi.StyleList{
42 List: ansi.StyleList{
43 LevelIndent: defaultMargin,
43 LevelIndent: defaultMargin,
44 StyleBlock: ansi.StyleBlock{
44 StyleBlock: ansi.StyleBlock{
45+ IndentToken: stringPtr(BaseStyle.Render(" ")),
45 StylePrimitive: ansi.StylePrimitive{
46 StylePrimitive: ansi.StylePrimitive{
46 Color: stringPtr(dark.Text().Hex),
47 Color: stringPtr(dark.Text().Hex),
47 },
48 },
@@ -496,3 +497,444 @@ var catppuccinLight = ansi.StyleConfig{
496 Color: stringPtr(light.Sapphire().Hex),
497 Color: stringPtr(light.Sapphire().Hex),
497 },
498 },
498}
499}
500+
501+func MarkdownTheme(focused bool) ansi.StyleConfig {
502+ if !focused {
503+ return ASCIIStyleConfig
504+ } else {
505+ return DraculaStyleConfig
506+ }
507+}
508+
509+const (
510+ defaultListIndent = 2
511+ defaultListLevelIndent = 4
512+)
513+
514+var ASCIIStyleConfig = ansi.StyleConfig{
515+ Document: ansi.StyleBlock{
516+ StylePrimitive: ansi.StylePrimitive{
517+ BackgroundColor: stringPtr(Background.Dark),
518+ },
519+ Indent: uintPtr(1),
520+ IndentToken: stringPtr(BaseStyle.Render(" ")),
521+ },
522+ BlockQuote: ansi.StyleBlock{
523+ StylePrimitive: ansi.StylePrimitive{
524+ BackgroundColor: stringPtr(Background.Dark),
525+ },
526+ Indent: uintPtr(1),
527+ IndentToken: stringPtr("| "),
528+ },
529+ Paragraph: ansi.StyleBlock{
530+ StylePrimitive: ansi.StylePrimitive{
531+ BackgroundColor: stringPtr(Background.Dark),
532+ },
533+ },
534+ List: ansi.StyleList{
535+ StyleBlock: ansi.StyleBlock{
536+ IndentToken: stringPtr(BaseStyle.Render(" ")),
537+ StylePrimitive: ansi.StylePrimitive{
538+ BackgroundColor: stringPtr(Background.Dark),
539+ },
540+ },
541+ LevelIndent: defaultListLevelIndent,
542+ },
543+ Heading: ansi.StyleBlock{
544+ StylePrimitive: ansi.StylePrimitive{
545+ BackgroundColor: stringPtr(Background.Dark),
546+ BlockSuffix: "\n",
547+ },
548+ },
549+ H1: ansi.StyleBlock{
550+ StylePrimitive: ansi.StylePrimitive{
551+ BackgroundColor: stringPtr(Background.Dark),
552+ Prefix: "# ",
553+ },
554+ },
555+ H2: ansi.StyleBlock{
556+ StylePrimitive: ansi.StylePrimitive{
557+ BackgroundColor: stringPtr(Background.Dark),
558+ Prefix: "## ",
559+ },
560+ },
561+ H3: ansi.StyleBlock{
562+ StylePrimitive: ansi.StylePrimitive{
563+ BackgroundColor: stringPtr(Background.Dark),
564+ Prefix: "### ",
565+ },
566+ },
567+ H4: ansi.StyleBlock{
568+ StylePrimitive: ansi.StylePrimitive{
569+ BackgroundColor: stringPtr(Background.Dark),
570+ Prefix: "#### ",
571+ },
572+ },
573+ H5: ansi.StyleBlock{
574+ StylePrimitive: ansi.StylePrimitive{
575+ BackgroundColor: stringPtr(Background.Dark),
576+ Prefix: "##### ",
577+ },
578+ },
579+ H6: ansi.StyleBlock{
580+ StylePrimitive: ansi.StylePrimitive{
581+ BackgroundColor: stringPtr(Background.Dark),
582+ Prefix: "###### ",
583+ },
584+ },
585+ Strikethrough: ansi.StylePrimitive{
586+ BackgroundColor: stringPtr(Background.Dark),
587+ BlockPrefix: "~~",
588+ BlockSuffix: "~~",
589+ },
590+ Emph: ansi.StylePrimitive{
591+ BackgroundColor: stringPtr(Background.Dark),
592+ BlockPrefix: "*",
593+ BlockSuffix: "*",
594+ },
595+ Strong: ansi.StylePrimitive{
596+ BackgroundColor: stringPtr(Background.Dark),
597+ BlockPrefix: "**",
598+ BlockSuffix: "**",
599+ },
600+ HorizontalRule: ansi.StylePrimitive{
601+ BackgroundColor: stringPtr(Background.Dark),
602+ Format: "\n--------\n",
603+ },
604+ Item: ansi.StylePrimitive{
605+ BlockPrefix: "• ",
606+ BackgroundColor: stringPtr(Background.Dark),
607+ },
608+ Enumeration: ansi.StylePrimitive{
609+ BlockPrefix: ". ",
610+ BackgroundColor: stringPtr(Background.Dark),
611+ },
612+ Task: ansi.StyleTask{
613+ Ticked: "[x] ",
614+ Unticked: "[ ] ",
615+ StylePrimitive: ansi.StylePrimitive{
616+ BackgroundColor: stringPtr(Background.Dark),
617+ },
618+ },
619+ ImageText: ansi.StylePrimitive{
620+ BackgroundColor: stringPtr(Background.Dark),
621+ Format: "Image: {{.text}} →",
622+ },
623+ Code: ansi.StyleBlock{
624+ StylePrimitive: ansi.StylePrimitive{
625+ BlockPrefix: "`",
626+ BlockSuffix: "`",
627+ BackgroundColor: stringPtr(Background.Dark),
628+ },
629+ },
630+ CodeBlock: ansi.StyleCodeBlock{
631+ StyleBlock: ansi.StyleBlock{
632+ StylePrimitive: ansi.StylePrimitive{
633+ BackgroundColor: stringPtr(Background.Dark),
634+ },
635+ Margin: uintPtr(defaultMargin),
636+ },
637+ },
638+ Table: ansi.StyleTable{
639+ StyleBlock: ansi.StyleBlock{
640+ StylePrimitive: ansi.StylePrimitive{
641+ BackgroundColor: stringPtr(Background.Dark),
642+ },
643+ IndentToken: stringPtr(BaseStyle.Render(" ")),
644+ },
645+ CenterSeparator: stringPtr("|"),
646+ ColumnSeparator: stringPtr("|"),
647+ RowSeparator: stringPtr("-"),
648+ },
649+ DefinitionDescription: ansi.StylePrimitive{
650+ BackgroundColor: stringPtr(Background.Dark),
651+ BlockPrefix: "\n* ",
652+ },
653+}
654+
655+var DraculaStyleConfig = ansi.StyleConfig{
656+ Document: ansi.StyleBlock{
657+ StylePrimitive: ansi.StylePrimitive{
658+ Color: stringPtr(Forground.Dark),
659+ BackgroundColor: stringPtr(Background.Dark),
660+ },
661+ Indent: uintPtr(defaultMargin),
662+ IndentToken: stringPtr(BaseStyle.Render(" ")),
663+ },
664+ BlockQuote: ansi.StyleBlock{
665+ StylePrimitive: ansi.StylePrimitive{
666+ Color: stringPtr("#f1fa8c"),
667+ Italic: boolPtr(true),
668+ BackgroundColor: stringPtr(Background.Dark),
669+ },
670+ Indent: uintPtr(defaultMargin),
671+ IndentToken: stringPtr(BaseStyle.Render(" ")),
672+ },
673+ Paragraph: ansi.StyleBlock{
674+ StylePrimitive: ansi.StylePrimitive{
675+ BackgroundColor: stringPtr(Background.Dark),
676+ },
677+ },
678+ List: ansi.StyleList{
679+ LevelIndent: defaultMargin,
680+ StyleBlock: ansi.StyleBlock{
681+ IndentToken: stringPtr(BaseStyle.Render(" ")),
682+ StylePrimitive: ansi.StylePrimitive{
683+ Color: stringPtr(Forground.Dark),
684+ BackgroundColor: stringPtr(Background.Dark),
685+ },
686+ },
687+ },
688+ Heading: ansi.StyleBlock{
689+ StylePrimitive: ansi.StylePrimitive{
690+ BlockSuffix: "\n",
691+ Color: stringPtr("#bd93f9"),
692+ Bold: boolPtr(true),
693+ BackgroundColor: stringPtr(Background.Dark),
694+ },
695+ },
696+ H1: ansi.StyleBlock{
697+ StylePrimitive: ansi.StylePrimitive{
698+ Prefix: "# ",
699+ BackgroundColor: stringPtr(Background.Dark),
700+ },
701+ },
702+ H2: ansi.StyleBlock{
703+ StylePrimitive: ansi.StylePrimitive{
704+ Prefix: "## ",
705+ BackgroundColor: stringPtr(Background.Dark),
706+ },
707+ },
708+ H3: ansi.StyleBlock{
709+ StylePrimitive: ansi.StylePrimitive{
710+ Prefix: "### ",
711+ BackgroundColor: stringPtr(Background.Dark),
712+ },
713+ },
714+ H4: ansi.StyleBlock{
715+ StylePrimitive: ansi.StylePrimitive{
716+ Prefix: "#### ",
717+ BackgroundColor: stringPtr(Background.Dark),
718+ },
719+ },
720+ H5: ansi.StyleBlock{
721+ StylePrimitive: ansi.StylePrimitive{
722+ Prefix: "##### ",
723+ BackgroundColor: stringPtr(Background.Dark),
724+ },
725+ },
726+ H6: ansi.StyleBlock{
727+ StylePrimitive: ansi.StylePrimitive{
728+ Prefix: "###### ",
729+ BackgroundColor: stringPtr(Background.Dark),
730+ },
731+ },
732+ Strikethrough: ansi.StylePrimitive{
733+ CrossedOut: boolPtr(true),
734+ BackgroundColor: stringPtr(Background.Dark),
735+ },
736+ Emph: ansi.StylePrimitive{
737+ Color: stringPtr("#f1fa8c"),
738+ Italic: boolPtr(true),
739+ BackgroundColor: stringPtr(Background.Dark),
740+ },
741+ Strong: ansi.StylePrimitive{
742+ Bold: boolPtr(true),
743+ Color: stringPtr("#ffb86c"),
744+ BackgroundColor: stringPtr(Background.Dark),
745+ },
746+ HorizontalRule: ansi.StylePrimitive{
747+ Color: stringPtr("#6272A4"),
748+ Format: "\n--------\n",
749+ BackgroundColor: stringPtr(Background.Dark),
750+ },
751+ Item: ansi.StylePrimitive{
752+ BlockPrefix: "• ",
753+ BackgroundColor: stringPtr(Background.Dark),
754+ },
755+ Enumeration: ansi.StylePrimitive{
756+ BlockPrefix: ". ",
757+ Color: stringPtr("#8be9fd"),
758+ BackgroundColor: stringPtr(Background.Dark),
759+ },
760+ Task: ansi.StyleTask{
761+ StylePrimitive: ansi.StylePrimitive{
762+ BackgroundColor: stringPtr(Background.Dark),
763+ },
764+ Ticked: "[✓] ",
765+ Unticked: "[ ] ",
766+ },
767+ Link: ansi.StylePrimitive{
768+ Color: stringPtr("#8be9fd"),
769+ Underline: boolPtr(true),
770+ BackgroundColor: stringPtr(Background.Dark),
771+ },
772+ LinkText: ansi.StylePrimitive{
773+ Color: stringPtr("#ff79c6"),
774+ BackgroundColor: stringPtr(Background.Dark),
775+ },
776+ Image: ansi.StylePrimitive{
777+ Color: stringPtr("#8be9fd"),
778+ Underline: boolPtr(true),
779+ BackgroundColor: stringPtr(Background.Dark),
780+ },
781+ ImageText: ansi.StylePrimitive{
782+ Color: stringPtr("#ff79c6"),
783+ Format: "Image: {{.text}} →",
784+ BackgroundColor: stringPtr(Background.Dark),
785+ },
786+ Code: ansi.StyleBlock{
787+ StylePrimitive: ansi.StylePrimitive{
788+ Color: stringPtr("#50fa7b"),
789+ BackgroundColor: stringPtr(Background.Dark),
790+ },
791+ },
792+ Text: ansi.StylePrimitive{
793+ BackgroundColor: stringPtr(Background.Dark),
794+ },
795+ DefinitionList: ansi.StyleBlock{},
796+ CodeBlock: ansi.StyleCodeBlock{
797+ StyleBlock: ansi.StyleBlock{
798+ StylePrimitive: ansi.StylePrimitive{
799+ Color: stringPtr("#ffb86c"),
800+ BackgroundColor: stringPtr(Background.Dark),
801+ },
802+ Margin: uintPtr(defaultMargin),
803+ },
804+ Chroma: &ansi.Chroma{
805+ NameOther: ansi.StylePrimitive{
806+ BackgroundColor: stringPtr(Background.Dark),
807+ },
808+ Literal: ansi.StylePrimitive{
809+ BackgroundColor: stringPtr(Background.Dark),
810+ },
811+ NameException: ansi.StylePrimitive{
812+ BackgroundColor: stringPtr(Background.Dark),
813+ },
814+ LiteralDate: ansi.StylePrimitive{
815+ BackgroundColor: stringPtr(Background.Dark),
816+ },
817+ Text: ansi.StylePrimitive{
818+ Color: stringPtr(Forground.Dark),
819+ BackgroundColor: stringPtr(Background.Dark),
820+ },
821+ Error: ansi.StylePrimitive{
822+ Color: stringPtr("#f8f8f2"),
823+ BackgroundColor: stringPtr("#ff5555"),
824+ },
825+ Comment: ansi.StylePrimitive{
826+ Color: stringPtr("#6272A4"),
827+ BackgroundColor: stringPtr(Background.Dark),
828+ },
829+ CommentPreproc: ansi.StylePrimitive{
830+ Color: stringPtr("#ff79c6"),
831+ BackgroundColor: stringPtr(Background.Dark),
832+ },
833+ Keyword: ansi.StylePrimitive{
834+ Color: stringPtr("#ff79c6"),
835+ BackgroundColor: stringPtr(Background.Dark),
836+ },
837+ KeywordReserved: ansi.StylePrimitive{
838+ Color: stringPtr("#ff79c6"),
839+ BackgroundColor: stringPtr(Background.Dark),
840+ },
841+ KeywordNamespace: ansi.StylePrimitive{
842+ Color: stringPtr("#ff79c6"),
843+ BackgroundColor: stringPtr(Background.Dark),
844+ },
845+ KeywordType: ansi.StylePrimitive{
846+ Color: stringPtr("#8be9fd"),
847+ BackgroundColor: stringPtr(Background.Dark),
848+ },
849+ Operator: ansi.StylePrimitive{
850+ Color: stringPtr("#ff79c6"),
851+ BackgroundColor: stringPtr(Background.Dark),
852+ },
853+ Punctuation: ansi.StylePrimitive{
854+ Color: stringPtr(Forground.Dark),
855+ BackgroundColor: stringPtr(Background.Dark),
856+ },
857+ Name: ansi.StylePrimitive{
858+ Color: stringPtr("#8be9fd"),
859+ BackgroundColor: stringPtr(Background.Dark),
860+ },
861+ NameBuiltin: ansi.StylePrimitive{
862+ Color: stringPtr("#8be9fd"),
863+ BackgroundColor: stringPtr(Background.Dark),
864+ },
865+ NameTag: ansi.StylePrimitive{
866+ Color: stringPtr("#ff79c6"),
867+ BackgroundColor: stringPtr(Background.Dark),
868+ },
869+ NameAttribute: ansi.StylePrimitive{
870+ Color: stringPtr("#50fa7b"),
871+ BackgroundColor: stringPtr(Background.Dark),
872+ },
873+ NameClass: ansi.StylePrimitive{
874+ Color: stringPtr("#8be9fd"),
875+ BackgroundColor: stringPtr(Background.Dark),
876+ },
877+ NameConstant: ansi.StylePrimitive{
878+ Color: stringPtr("#bd93f9"),
879+ BackgroundColor: stringPtr(Background.Dark),
880+ },
881+ NameDecorator: ansi.StylePrimitive{
882+ Color: stringPtr("#50fa7b"),
883+ BackgroundColor: stringPtr(Background.Dark),
884+ },
885+ NameFunction: ansi.StylePrimitive{
886+ Color: stringPtr("#50fa7b"),
887+ BackgroundColor: stringPtr(Background.Dark),
888+ },
889+ LiteralNumber: ansi.StylePrimitive{
890+ Color: stringPtr("#6EEFC0"),
891+ BackgroundColor: stringPtr(Background.Dark),
892+ },
893+ LiteralString: ansi.StylePrimitive{
894+ Color: stringPtr("#f1fa8c"),
895+ BackgroundColor: stringPtr(Background.Dark),
896+ },
897+ LiteralStringEscape: ansi.StylePrimitive{
898+ Color: stringPtr("#ff79c6"),
899+ BackgroundColor: stringPtr(Background.Dark),
900+ },
901+ GenericDeleted: ansi.StylePrimitive{
902+ Color: stringPtr("#ff5555"),
903+ BackgroundColor: stringPtr(Background.Dark),
904+ },
905+ GenericEmph: ansi.StylePrimitive{
906+ Color: stringPtr("#f1fa8c"),
907+ Italic: boolPtr(true),
908+ BackgroundColor: stringPtr(Background.Dark),
909+ },
910+ GenericInserted: ansi.StylePrimitive{
911+ Color: stringPtr("#50fa7b"),
912+ BackgroundColor: stringPtr(Background.Dark),
913+ },
914+ GenericStrong: ansi.StylePrimitive{
915+ Color: stringPtr("#ffb86c"),
916+ Bold: boolPtr(true),
917+ BackgroundColor: stringPtr(Background.Dark),
918+ },
919+ GenericSubheading: ansi.StylePrimitive{
920+ Color: stringPtr("#bd93f9"),
921+ BackgroundColor: stringPtr(Background.Dark),
922+ },
923+ Background: ansi.StylePrimitive{
924+ BackgroundColor: stringPtr(Background.Dark),
925+ },
926+ },
927+ },
928+ Table: ansi.StyleTable{
929+ StyleBlock: ansi.StyleBlock{
930+ StylePrimitive: ansi.StylePrimitive{
931+ BackgroundColor: stringPtr(Background.Dark),
932+ },
933+ IndentToken: stringPtr(BaseStyle.Render(" ")),
934+ },
935+ },
936+ DefinitionDescription: ansi.StylePrimitive{
937+ BlockPrefix: "\n* ",
938+ BackgroundColor: stringPtr(Background.Dark),
939+ },
940+}