on main help sign in
chore: initial logs changes
3b840adKujtim Hoxha
internal/config/init.go+2−2
@@ -63,13 +63,13 @@ func crushMdExists(dir string) (bool, error) {
63 if entry.IsDir() {
63 if entry.IsDir() {
64 continue
64 continue
65 }
65 }
66−
66+
67 name := strings.ToLower(entry.Name())
67 name := strings.ToLower(entry.Name())
68 if name == "crush.md" {
68 if name == "crush.md" {
69 return true, nil
69 return true, nil
70 }
70 }
71 }
71 }
72−
72+
73 return false, nil
73 return false, nil
74}
74}
75
75
internal/tui/components/chat/editor/editor.go−1
@@ -360,7 +360,6 @@ func (c *editorCmp) Blur() tea.Cmd {
360
360
361// Focus implements Container.
361// Focus implements Container.
362func (c *editorCmp) Focus() tea.Cmd {
362func (c *editorCmp) Focus() tea.Cmd {
363− logging.Info("Focusing editor textarea")
364 return c.textarea.Focus()
363 return c.textarea.Focus()
365}
364}
366
365
internal/tui/components/core/helpers.go+13−13
@@ -78,39 +78,39 @@ func Status(ops StatusOpts, width int) string {
78}
78}
79
79
80type ButtonOpts struct {
80type ButtonOpts struct {
81− Text string
81+ Text string
82− UnderlineIndex int // Index of character to underline (0-based)
82+ UnderlineIndex int // Index of character to underline (0-based)
83− Selected bool // Whether this button is selected
83+ Selected bool // Whether this button is selected
84}
84}
85
85
86// SelectableButton creates a button with an underlined character and selection state
86// SelectableButton creates a button with an underlined character and selection state
87func SelectableButton(opts ButtonOpts) string {
87func SelectableButton(opts ButtonOpts) string {
88 t := styles.CurrentTheme()
88 t := styles.CurrentTheme()
89−
89+
90 // Base style for the button
90 // Base style for the button
91 buttonStyle := t.S().Text
91 buttonStyle := t.S().Text
92−
92+
93 // Apply selection styling
93 // Apply selection styling
94 if opts.Selected {
94 if opts.Selected {
95 buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
95 buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
96 } else {
96 } else {
97 buttonStyle = buttonStyle.Background(t.BgSubtle)
97 buttonStyle = buttonStyle.Background(t.BgSubtle)
98 }
98 }
99−
99+
100 // Create the button text with underlined character
100 // Create the button text with underlined character
101 text := opts.Text
101 text := opts.Text
102 if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
102 if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
103 before := text[:opts.UnderlineIndex]
103 before := text[:opts.UnderlineIndex]
104 underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
104 underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
105 after := text[opts.UnderlineIndex+1:]
105 after := text[opts.UnderlineIndex+1:]
106−
106+
107− message := buttonStyle.Render(before) +
107+ message := buttonStyle.Render(before) +
108− buttonStyle.Underline(true).Render(underlined) +
108+ buttonStyle.Underline(true).Render(underlined) +
109 buttonStyle.Render(after)
109 buttonStyle.Render(after)
110−
110+
111 return buttonStyle.Padding(0, 2).Render(message)
111 return buttonStyle.Padding(0, 2).Render(message)
112 }
112 }
113−
113+
114 // Fallback if no underline index specified
114 // Fallback if no underline index specified
115 return buttonStyle.Padding(0, 2).Render(text)
115 return buttonStyle.Padding(0, 2).Render(text)
116}
116}
@@ -120,7 +120,7 @@ func SelectableButtons(buttons []ButtonOpts, spacing string) string {
120 if spacing == "" {
120 if spacing == "" {
121 spacing = " "
121 spacing = " "
122 }
122 }
123−
123+
124 var parts []string
124 var parts []string
125 for i, button := range buttons {
125 for i, button := range buttons {
126 parts = append(parts, SelectableButton(button))
126 parts = append(parts, SelectableButton(button))
@@ -128,6 +128,6 @@ func SelectableButtons(buttons []ButtonOpts, spacing string) string {
128 parts = append(parts, spacing)
128 parts = append(parts, spacing)
129 }
129 }
130 }
130 }
131−
131+
132 return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
132 return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
133}
133}
internal/tui/components/dialogs/compact/compact.go−1
@@ -263,4 +263,3 @@ func (c *compactDialogCmp) Position() (int, int) {
263func (c *compactDialogCmp) ID() dialogs.DialogID {
263func (c *compactDialogCmp) ID() dialogs.DialogID {
264 return CompactDialogID
264 return CompactDialogID
265}
265}
266−
internal/tui/components/dialogs/compact/keys.go+1−1
@@ -58,4 +58,4 @@ func (k KeyMap) ShortHelp() []key.Binding {
58 k.Select,
58 k.Select,
59 k.Close,
59 k.Close,
60 }
60 }
61−}
61+}
internal/tui/components/logs/details.go+66−25
@@ -52,43 +52,55 @@ func (i *detailCmp) updateContent() {
52 var content strings.Builder
52 var content strings.Builder
53 t := styles.CurrentTheme()
53 t := styles.CurrentTheme()
54
54
55− // Format the header with timestamp and level
55+ if i.currentLog.ID == "" {
56− timeStyle := t.S().Muted
56+ content.WriteString(t.S().Muted.Render("No log selected"))
57+ i.viewport.SetContent(content.String())
58+ return
59+ }
60+
61+ // Level badge with background color
57 levelStyle := getLevelStyle(i.currentLog.Level)
62 levelStyle := getLevelStyle(i.currentLog.Level)
63+ levelBadge := levelStyle.Padding(0, 1).Render(strings.ToUpper(i.currentLog.Level))
58
64
65+ // Timestamp with relative time
66+ timeStr := i.currentLog.Time.Format("2006-01-05 15:04:05 UTC")
67+ relativeTime := getRelativeTime(i.currentLog.Time)
68+ timeStyle := t.S().Muted
69+
70+ // Header line
59 header := lipgloss.JoinHorizontal(
71 header := lipgloss.JoinHorizontal(
60− lipgloss.Center,
72+ lipgloss.Left,
73+ timeStr,
74+ " ",
61− timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
75+ timeStyle.Render(relativeTime),
62− " ",
63− levelStyle.Render(i.currentLog.Level),
64 )
76 )
65
77
66− content.WriteString(lipgloss.NewStyle().Bold(true).Render(header))
78+ content.WriteString(levelBadge)
79+ content.WriteString("\n\n")
80+ content.WriteString(header)
67 content.WriteString("\n\n")
81 content.WriteString("\n\n")
68
82
69− // Message with styling
83+ // Message section
70− messageStyle := t.S().Text.Bold(true)
84+ messageHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
71− content.WriteString(messageStyle.Render("Message:"))
85+ content.WriteString(messageHeaderStyle.Render("Message"))
72 content.WriteString("\n")
86 content.WriteString("\n")
73− content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
87+ content.WriteString(i.currentLog.Message)
74 content.WriteString("\n\n")
88 content.WriteString("\n\n")
75
89
76 // Attributes section
90 // Attributes section
77 if len(i.currentLog.Attributes) > 0 {
91 if len(i.currentLog.Attributes) > 0 {
78− attrHeaderStyle := t.S().Text.Bold(true)
92+ attrHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
79− content.WriteString(attrHeaderStyle.Render("Attributes:"))
93+ content.WriteString(attrHeaderStyle.Render("Attributes"))
80 content.WriteString("\n")
94 content.WriteString("\n")
81
95
82− // Create a table-like display for attributes
83− keyStyle := t.S().Base.Foreground(t.Primary).Bold(true)
84− valueStyle := t.S().Text
85−
86 for _, attr := range i.currentLog.Attributes {
96 for _, attr := range i.currentLog.Attributes {
97+ keyStyle := t.S().Base.Foreground(t.Accent)
98+ valueStyle := t.S().Text
87 attrLine := fmt.Sprintf("%s: %s",
99 attrLine := fmt.Sprintf("%s: %s",
88 keyStyle.Render(attr.Key),
100 keyStyle.Render(attr.Key),
89 valueStyle.Render(attr.Value),
101 valueStyle.Render(attr.Value),
90 )
102 )
91− content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine))
103+ content.WriteString(attrLine)
92 content.WriteString("\n")
104 content.WriteString("\n")
93 }
105 }
94 }
106 }
@@ -102,20 +114,48 @@ func getLevelStyle(level string) lipgloss.Style {
102
114
103 switch strings.ToLower(level) {
115 switch strings.ToLower(level) {
104 case "info":
116 case "info":
105− return style.Foreground(t.Info)
117+ return style.Foreground(t.White).Background(t.Info)
106 case "warn", "warning":
118 case "warn", "warning":
107− return style.Foreground(t.Warning)
119+ return style.Foreground(t.White).Background(t.Warning)
108 case "error", "err":
120 case "error", "err":
109− return style.Foreground(t.Error)
121+ return style.Foreground(t.White).Background(t.Error)
110 case "debug":
122 case "debug":
111− return style.Foreground(t.Success)
123+ return style.Foreground(t.White).Background(t.Success)
124+ case "fatal":
125+ return style.Foreground(t.White).Background(t.Error)
112 default:
126 default:
113 return style.Foreground(t.FgBase)
127 return style.Foreground(t.FgBase)
114 }
128 }
115}
129}
116
130
131+func getRelativeTime(logTime time.Time) string {
132+ now := time.Now()
133+ diff := now.Sub(logTime)
134+
135+ if diff < time.Minute {
136+ return fmt.Sprintf("%ds ago", int(diff.Seconds()))
137+ } else if diff < time.Hour {
138+ return fmt.Sprintf("%dm ago", int(diff.Minutes()))
139+ } else if diff < 24*time.Hour {
140+ return fmt.Sprintf("%dh ago", int(diff.Hours()))
141+ } else if diff < 30*24*time.Hour {
142+ return fmt.Sprintf("%dd ago", int(diff.Hours()/24))
143+ } else if diff < 365*24*time.Hour {
144+ return fmt.Sprintf("%dmo ago", int(diff.Hours()/(24*30)))
145+ } else {
146+ return fmt.Sprintf("%dy ago", int(diff.Hours()/(24*365)))
147+ }
148+}
149+
117func (i *detailCmp) View() tea.View {
150func (i *detailCmp) View() tea.View {
151+ t := styles.CurrentTheme()
152+ style := t.S().Base.
153+ BorderStyle(lipgloss.RoundedBorder()).
154+ BorderForeground(t.BorderFocus).
155+ Width(i.width - 2). // Adjust width for border
156+ Height(i.height - 2). // Adjust height for border
157+ Padding(1)
118− return tea.NewView(i.viewport.View())
158+ return tea.NewView(style.Render(i.viewport.View()))
119}
159}
120
160
121func (i *detailCmp) GetSize() (int, int) {
161func (i *detailCmp) GetSize() (int, int) {
@@ -123,10 +163,11 @@ func (i *detailCmp) GetSize() (int, int) {
123}
163}
124
164
125func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
165func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
166+ logging.Info("Setting size for detail component", "width", width, "height", height)
126 i.width = width
167 i.width = width
127 i.height = height
168 i.height = height
128− i.viewport.SetWidth(i.width)
169+ i.viewport.SetWidth(i.width - 4)
129− i.viewport.SetHeight(i.height)
170+ i.viewport.SetHeight(i.height - 4)
130 i.updateContent()
171 i.updateContent()
131 return nil
172 return nil
132}
173}
internal/tui/components/logs/table.go+111−44
@@ -1,8 +1,9 @@
1package logs
1package logs
2
2
3import (
3import (
4− "encoding/json"
4+ "fmt"
5 "slices"
5 "slices"
6+ "strings"
6
7
7 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/table"
9 "github.com/charmbracelet/bubbles/v2/table"
@@ -12,6 +13,7 @@ import (
12 "github.com/charmbracelet/crush/internal/tui/layout"
13 "github.com/charmbracelet/crush/internal/tui/layout"
13 "github.com/charmbracelet/crush/internal/tui/styles"
14 "github.com/charmbracelet/crush/internal/tui/styles"
14 "github.com/charmbracelet/crush/internal/tui/util"
15 "github.com/charmbracelet/crush/internal/tui/util"
16+ "github.com/charmbracelet/lipgloss/v2"
15)
17)
16
18
17type TableComponent interface {
19type TableComponent interface {
@@ -22,48 +24,80 @@ type TableComponent interface {
22
24
23type tableCmp struct {
25type tableCmp struct {
24 table table.Model
26 table table.Model
27+ logs []logging.LogMessage
25}
28}
26
29
27type selectedLogMsg logging.LogMessage
30type selectedLogMsg logging.LogMessage
28
31
29func (i *tableCmp) Init() tea.Cmd {
32func (i *tableCmp) Init() tea.Cmd {
33+ i.logs = logging.List()
30 i.setRows()
34 i.setRows()
31 return nil
35 return nil
32}
36}
33
37
34func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
38func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
35 var cmds []tea.Cmd
39 var cmds []tea.Cmd
36− switch msg.(type) {
40+ switch msg := msg.(type) {
37 case pubsub.Event[logging.LogMessage]:
41 case pubsub.Event[logging.LogMessage]:
38− i.setRows()
42+ return i, func() tea.Msg {
39− return i, nil
43+ if msg.Type == pubsub.CreatedEvent {
44+ rows := i.table.Rows()
45+ for _, row := range rows {
46+ if row[1] == msg.Payload.ID {
47+ return nil // If the log already exists, do not add it again
48+ }
49+ }
50+ i.logs = append(i.logs, msg.Payload)
51+ i.table.SetRows(
52+ append(
53+ []table.Row{
54+ logToRow(msg.Payload),
55+ },
56+ i.table.Rows()...,
57+ ),
58+ )
59+ }
60+ return selectedLogMsg(msg.Payload)
61+ }
40 }
62 }
41− prevSelectedRow := i.table.SelectedRow()
42 t, cmd := i.table.Update(msg)
63 t, cmd := i.table.Update(msg)
43 cmds = append(cmds, cmd)
64 cmds = append(cmds, cmd)
44 i.table = t
65 i.table = t
45− selectedRow := i.table.SelectedRow()
66+
46− if selectedRow != nil {
67+ cmds = append(cmds, func() tea.Msg {
47− if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] {
48− var log logging.LogMessage
49− for _, row := range logging.List() {
68+ for _, log := range logging.List() {
50− if row.ID == selectedRow[0] {
69+ if log.ID == i.table.SelectedRow()[1] {
51− log = row
70+ // If the selected row matches the log ID, return the selected log message
52− break
53− }
54− }
55− if log.ID != "" {
56− cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
71+ return selectedLogMsg(log)
57 }
72 }
58 }
73 }
74+ return nil
59− }
75+ })
60 return i, tea.Batch(cmds...)
76 return i, tea.Batch(cmds...)
61}
77}
62
78
63func (i *tableCmp) View() tea.View {
79func (i *tableCmp) View() tea.View {
64 t := styles.CurrentTheme()
80 t := styles.CurrentTheme()
65 defaultStyles := table.DefaultStyles()
81 defaultStyles := table.DefaultStyles()
82+
83+ // Header styling
84+ defaultStyles.Header = defaultStyles.Header.
85+ Foreground(t.Primary).
86+ Bold(true).
87+ BorderStyle(lipgloss.NormalBorder()).
88+ BorderBottom(true).
89+ BorderForeground(t.Border)
90+
91+ // Selected row styling
66− defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary)
92+ defaultStyles.Selected = defaultStyles.Selected.
93+ Foreground(t.FgSelected).
94+ Background(t.Primary).
95+ Bold(false)
96+
97+ // Cell styling
98+ defaultStyles.Cell = defaultStyles.Cell.
99+ Foreground(t.FgBase)
100+
67 i.table.SetStyles(defaultStyles)
101 i.table.SetStyles(defaultStyles)
68 return tea.NewView(i.table.View())
102 return tea.NewView(i.table.View())
69}
103}
@@ -75,12 +109,30 @@ func (i *tableCmp) GetSize() (int, int) {
75func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
109func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
76 i.table.SetWidth(width)
110 i.table.SetWidth(width)
77 i.table.SetHeight(height)
111 i.table.SetHeight(height)
78− cloumns := i.table.Columns()
112+
79− for i, col := range cloumns {
113+ columnWidth := (width - 10) / 4
80− col.Width = (width / len(cloumns)) - 2
114+ i.table.SetColumns([]table.Column{
81− cloumns[i] = col
115+ {
116+ Title: "Level",
117+ Width: 10,
118+ },
119+ {
120+ Title: "ID",
121+ Width: columnWidth,
122+ },
123+ {
124+ Title: "Time",
125+ Width: columnWidth,
126+ },
127+ {
128+ Title: "Message",
129+ Width: columnWidth,
82− }
130+ },
83− i.table.SetColumns(cloumns)
131+ {
132+ Title: "Attributes",
133+ Width: columnWidth,
134+ },
135+ })
84 return nil
136 return nil
85}
137}
86
138
@@ -91,39 +143,54 @@ func (i *tableCmp) BindingKeys() []key.Binding {
91func (i *tableCmp) setRows() {
143func (i *tableCmp) setRows() {
92 rows := []table.Row{}
144 rows := []table.Row{}
93
145
94− logs := logging.List()
95− slices.SortFunc(logs, func(a, b logging.LogMessage) int {
146+ slices.SortFunc(i.logs, func(a, b logging.LogMessage) int {
96 if a.Time.Before(b.Time) {
147 if a.Time.Before(b.Time) {
97− return 1
148+ return -1
98 }
149 }
99 if a.Time.After(b.Time) {
150 if a.Time.After(b.Time) {
100− return -1
151+ return 1
101 }
152 }
102 return 0
153 return 0
103 })
154 })
104
155
105− for _, log := range logs {
156+ for _, log := range i.logs {
106− bm, _ := json.Marshal(log.Attributes)
157+ rows = append(rows, logToRow(log))
158+ }
159+ i.table.SetRows(rows)
160+}
107
161
108− row := table.Row{
162+func logToRow(log logging.LogMessage) table.Row {
109− log.ID,
163+ // Format attributes as JSON string
110− log.Time.Format("15:04:05"),
164+ var attrStr string
111− log.Level,
165+ if len(log.Attributes) > 0 {
112− log.Message,
166+ var parts []string
113− string(bm),
167+ for _, attr := range log.Attributes {
168+ parts = append(parts, fmt.Sprintf(`{"Key":"%s","Value":"%s"}`, attr.Key, attr.Value))
114 }
169 }
115− rows = append(rows, row)
170+ attrStr = "[" + strings.Join(parts, ",") + "]"
171+ }
172+
173+ // Format time with relative time
174+ timeStr := log.Time.Format("2006-01-05 15:04:05 UTC")
175+ relativeTime := getRelativeTime(log.Time)
176+ fullTimeStr := timeStr + " " + relativeTime
177+
178+ return table.Row{
179+ strings.ToUpper(log.Level),
180+ log.ID,
181+ fullTimeStr,
182+ log.Message,
183+ attrStr,
116 }
184 }
117− i.table.SetRows(rows)
118}
185}
119
186
120func NewLogsTable() TableComponent {
187func NewLogsTable() TableComponent {
121 columns := []table.Column{
188 columns := []table.Column{
122− {Title: "ID", Width: 4},
189+ {Title: "Level"},
190+ {Title: "ID"},
123− {Title: "Time", Width: 4},
191+ {Title: "Time"},
124− {Title: "Level", Width: 10},
125− {Title: "Message", Width: 10},
192+ {Title: "Message"},
126− {Title: "Attributes", Width: 10},
193+ {Title: "Attributes"},
127 }
194 }
128
195
129 tableModel := table.New(
196 tableModel := table.New(
internal/tui/page/chat/chat.go−2
@@ -8,7 +8,6 @@ import (
8 "github.com/charmbracelet/crush/internal/app"
8 "github.com/charmbracelet/crush/internal/app"
9 "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/llm/models"
10 "github.com/charmbracelet/crush/internal/llm/models"
11− "github.com/charmbracelet/crush/internal/logging"
12 "github.com/charmbracelet/crush/internal/message"
11 "github.com/charmbracelet/crush/internal/message"
13 "github.com/charmbracelet/crush/internal/session"
12 "github.com/charmbracelet/crush/internal/session"
14 "github.com/charmbracelet/crush/internal/tui/components/chat"
13 "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -99,7 +98,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
99 return p, util.ReportWarn("File attachments are not supported by the current model: " + string(selectedModelID))
98 return p, util.ReportWarn("File attachments are not supported by the current model: " + string(selectedModelID))
100 }
99 }
101 case key.Matches(msg, p.keyMap.Tab):
100 case key.Matches(msg, p.keyMap.Tab):
102− logging.Info("Tab key pressed, toggling chat focus")
103 if p.session.ID == "" {
101 if p.session.ID == "" {
104 return p, nil
102 return p, nil
105 }
103 }
internal/tui/page/logs/keys.gonew file+37
@@ -0,0 +1,37 @@
1+package logs
2+
3+import (
4+ "github.com/charmbracelet/bubbles/v2/key"
5+ "github.com/charmbracelet/crush/internal/tui/layout"
6+)
7+
8+type KeyMap struct {
9+ Back key.Binding
10+}
11+
12+func DefaultKeyMap() KeyMap {
13+ return KeyMap{
14+ Back: key.NewBinding(
15+ key.WithKeys("esc", "backspace"),
16+ key.WithHelp("esc/backspace", "back to chat"),
17+ ),
18+ }
19+}
20+
21+// FullHelp implements help.KeyMap.
22+func (k KeyMap) FullHelp() [][]key.Binding {
23+ m := [][]key.Binding{}
24+ slice := layout.KeyMapToSlice(k)
25+ for i := 0; i < len(slice); i += 4 {
26+ end := min(i+4, len(slice))
27+ m = append(m, slice[i:end])
28+ }
29+ return m
30+}
31+
32+// ShortHelp implements help.KeyMap.
33+func (k KeyMap) ShortHelp() []key.Binding {
34+ return []key.Binding{
35+ k.Back,
36+ }
37+}
internal/tui/page/logs.go → internal/tui/page/logs/logs.gorenamed+30−18
@@ -1,26 +1,30 @@
1−package page
1+package logs
2
2
3import (
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
5 tea "github.com/charmbracelet/bubbletea/v2"
6− "github.com/charmbracelet/crush/internal/tui/components/logs"
6+ "github.com/charmbracelet/crush/internal/tui/components/core"
7+ logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs"
7 "github.com/charmbracelet/crush/internal/tui/layout"
8 "github.com/charmbracelet/crush/internal/tui/layout"
9+ "github.com/charmbracelet/crush/internal/tui/page"
10+ "github.com/charmbracelet/crush/internal/tui/page/chat"
8 "github.com/charmbracelet/crush/internal/tui/styles"
11 "github.com/charmbracelet/crush/internal/tui/styles"
9 "github.com/charmbracelet/crush/internal/tui/util"
12 "github.com/charmbracelet/crush/internal/tui/util"
10 "github.com/charmbracelet/lipgloss/v2"
13 "github.com/charmbracelet/lipgloss/v2"
11)
14)
12
15
13−var LogsPage PageID = "logs"
16+var LogsPage page.PageID = "logs"
14
17
15type LogPage interface {
18type LogPage interface {
16 util.Model
19 util.Model
17 layout.Sizeable
20 layout.Sizeable
18− layout.Bindings
19}
21}
22+
20type logsPage struct {
23type logsPage struct {
21 width, height int
24 width, height int
22− table layout.Container
25+ table logsComponents.TableComponent
23− details layout.Container
26+ details logsComponents.DetailComponent
27+ keyMap KeyMap
24}
28}
25
29
26func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
30func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -30,34 +34,39 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
30 p.width = msg.Width
34 p.width = msg.Width
31 p.height = msg.Height
35 p.height = msg.Height
32 return p, p.SetSize(msg.Width, msg.Height)
36 return p, p.SetSize(msg.Width, msg.Height)
37+ case tea.KeyMsg:
38+ switch {
39+ case key.Matches(msg, p.keyMap.Back):
40+ return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPage})
41+ }
33 }
42 }
34
43
35 table, cmd := p.table.Update(msg)
44 table, cmd := p.table.Update(msg)
36 cmds = append(cmds, cmd)
45 cmds = append(cmds, cmd)
37− p.table = table.(layout.Container)
46+ p.table = table.(logsComponents.TableComponent)
38 details, cmd := p.details.Update(msg)
47 details, cmd := p.details.Update(msg)
39 cmds = append(cmds, cmd)
48 cmds = append(cmds, cmd)
40− p.details = details.(layout.Container)
49+ p.details = details.(logsComponents.DetailComponent)
41
50
42 return p, tea.Batch(cmds...)
51 return p, tea.Batch(cmds...)
43}
52}
44
53
45func (p *logsPage) View() tea.View {
54func (p *logsPage) View() tea.View {
46− style := styles.CurrentTheme().S().Base.Width(p.width).Height(p.height)
55+ baseStyle := styles.CurrentTheme().S().Base
56+ style := baseStyle.Width(p.width).Height(p.height).Padding(1)
57+ title := core.Title("Logs", p.width-2)
58+
47 return tea.NewView(
59 return tea.NewView(
48 style.Render(
60 style.Render(
49 lipgloss.JoinVertical(lipgloss.Top,
61 lipgloss.JoinVertical(lipgloss.Top,
50− p.table.View().String(),
62+ title,
51 p.details.View().String(),
63 p.details.View().String(),
64+ p.table.View().String(),
52 ),
65 ),
53 ),
66 ),
54 )
67 )
55}
68}
56
69
57−func (p *logsPage) BindingKeys() []key.Binding {
58− return p.table.BindingKeys()
59−}
60−
61// GetSize implements LogPage.
70// GetSize implements LogPage.
62func (p *logsPage) GetSize() (int, int) {
71func (p *logsPage) GetSize() (int, int) {
63 return p.width, p.height
72 return p.width, p.height
@@ -67,9 +76,11 @@ func (p *logsPage) GetSize() (int, int) {
67func (p *logsPage) SetSize(width int, height int) tea.Cmd {
76func (p *logsPage) SetSize(width int, height int) tea.Cmd {
68 p.width = width
77 p.width = width
69 p.height = height
78 p.height = height
79+ availableHeight := height - 2 // Padding for top and bottom
80+ availableHeight -= 1 // title height
70 return tea.Batch(
81 return tea.Batch(
71− p.table.SetSize(width, height/2),
82+ p.table.SetSize(width-2, availableHeight/2),
72− p.details.SetSize(width, height/2),
83+ p.details.SetSize(width-2, availableHeight/2),
73 )
84 )
74}
85}
75
86
@@ -82,7 +93,8 @@ func (p *logsPage) Init() tea.Cmd {
82
93
83func NewLogsPage() LogPage {
94func NewLogsPage() LogPage {
84 return &logsPage{
95 return &logsPage{
85− table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll()),
96+ details: logsComponents.NewLogsDetails(),
86− details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll()),
97+ table: logsComponents.NewLogsTable(),
98+ keyMap: DefaultKeyMap(),
87 }
99 }
88}
100}
internal/tui/tui.go+4−4
@@ -27,6 +27,7 @@ import (
27 "github.com/charmbracelet/crush/internal/tui/layout"
27 "github.com/charmbracelet/crush/internal/tui/layout"
28 "github.com/charmbracelet/crush/internal/tui/page"
28 "github.com/charmbracelet/crush/internal/tui/page"
29 "github.com/charmbracelet/crush/internal/tui/page/chat"
29 "github.com/charmbracelet/crush/internal/tui/page/chat"
30+ "github.com/charmbracelet/crush/internal/tui/page/logs"
30 "github.com/charmbracelet/crush/internal/tui/styles"
31 "github.com/charmbracelet/crush/internal/tui/styles"
31 "github.com/charmbracelet/crush/internal/tui/util"
32 "github.com/charmbracelet/crush/internal/tui/util"
32 "github.com/charmbracelet/lipgloss/v2"
33 "github.com/charmbracelet/lipgloss/v2"
@@ -137,7 +138,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
137 cmds = append(cmds, statusCmd)
138 cmds = append(cmds, statusCmd)
138
139
139 // If the current page is logs, update the logs view
140 // If the current page is logs, update the logs view
140− if a.currentPage == page.LogsPage {
141+ if a.currentPage == logs.LogsPage {
141 updated, pageCmd := a.pages[a.currentPage].Update(msg)
142 updated, pageCmd := a.pages[a.currentPage].Update(msg)
142 a.pages[a.currentPage] = updated.(util.Model)
143 a.pages[a.currentPage] = updated.(util.Model)
143 cmds = append(cmds, pageCmd)
144 cmds = append(cmds, pageCmd)
@@ -328,7 +329,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
328 return tea.Sequence(cmds...)
329 return tea.Sequence(cmds...)
329 // Page navigation
330 // Page navigation
330 case key.Matches(msg, a.keyMap.Logs):
331 case key.Matches(msg, a.keyMap.Logs):
331− return a.moveToPage(page.LogsPage)
332+ return a.moveToPage(logs.LogsPage)
332
333
333 default:
334 default:
334 if a.dialog.HasDialogs() {
335 if a.dialog.HasDialogs() {
@@ -379,7 +380,6 @@ func (a *appModel) View() tea.View {
379 lipgloss.NewLayer(appView),
380 lipgloss.NewLayer(appView),
380 }
381 }
381 if a.dialog.HasDialogs() {
382 if a.dialog.HasDialogs() {
382− logging.Info("Rendering dialogs")
383 layers = append(
383 layers = append(
384 layers,
384 layers,
385 a.dialog.GetLayers()...,
385 a.dialog.GetLayers()...,
@@ -424,7 +424,7 @@ func New(app *app.App) tea.Model {
424
424
425 pages: map[page.PageID]util.Model{
425 pages: map[page.PageID]util.Model{
426 chat.ChatPage: chat.NewChatPage(app),
426 chat.ChatPage: chat.NewChatPage(app),
427− page.LogsPage: page.NewLogsPage(),
427+ logs.LogsPage: logs.NewLogsPage(),
428 },
428 },
429
429
430 dialog: dialogs.NewDialogCmp(),
430 dialog: dialogs.NewDialogCmp(),
todos.md+6−3
@@ -11,9 +11,9 @@
11
11
12- [x] Cleanup Commands
12- [x] Cleanup Commands
13- [x] Sessions dialog
13- [x] Sessions dialog
14−- [ ] Models
14+- [x] Models
15−- [~] Move sessions and model dialog to the commands
15+- [x] Move sessions and model dialog to the commands
16−- [ ] Add sessions shortuct
16+- [x] Add sessions shortuct
17- [ ] Add all posible actions to the commands
17- [ ] Add all posible actions to the commands
18
18
19## Investigate
19## Investigate
@@ -24,3 +24,6 @@
24## Messages
24## Messages
25
25
26- [ ] Fix issue with numbers (padding)
26- [ ] Fix issue with numbers (padding)
27+- [ ] Run tools in parallel and add the responses in parallel
28+- [ ] Handle parallel permission calls
29+- [ ] Weird behavior sometimes the message does not update