chore: initial logs changes

Kujtim Hoxha created

Change summary

internal/config/init.go                            |   4 
internal/tui/components/chat/editor/editor.go      |   1 
internal/tui/components/core/helpers.go            |  26 +-
internal/tui/components/dialogs/compact/compact.go |   1 
internal/tui/components/dialogs/compact/keys.go    |   2 
internal/tui/components/logs/details.go            |  91 ++++++--
internal/tui/components/logs/table.go              | 155 +++++++++++----
internal/tui/page/chat/chat.go                     |   2 
internal/tui/page/logs/keys.go                     |  37 +++
internal/tui/page/logs/logs.go                     |  48 +++-
internal/tui/tui.go                                |   8 
todos.md                                           |   9 
12 files changed, 270 insertions(+), 114 deletions(-)

Detailed changes

internal/config/init.go 🔗

@@ -63,13 +63,13 @@ func crushMdExists(dir string) (bool, error) {
 		if entry.IsDir() {
 			continue
 		}
-		
+
 		name := strings.ToLower(entry.Name())
 		if name == "crush.md" {
 			return true, nil
 		}
 	}
-	
+
 	return false, nil
 }
 

internal/tui/components/chat/editor/editor.go 🔗

@@ -360,7 +360,6 @@ func (c *editorCmp) Blur() tea.Cmd {
 
 // Focus implements Container.
 func (c *editorCmp) Focus() tea.Cmd {
-	logging.Info("Focusing editor textarea")
 	return c.textarea.Focus()
 }
 

internal/tui/components/core/helpers.go 🔗

@@ -78,39 +78,39 @@ func Status(ops StatusOpts, width int) string {
 }
 
 type ButtonOpts struct {
-	Text            string
-	UnderlineIndex  int  // Index of character to underline (0-based)
-	Selected        bool // Whether this button is selected
+	Text           string
+	UnderlineIndex int  // Index of character to underline (0-based)
+	Selected       bool // Whether this button is selected
 }
 
 // SelectableButton creates a button with an underlined character and selection state
 func SelectableButton(opts ButtonOpts) string {
 	t := styles.CurrentTheme()
-	
+
 	// Base style for the button
 	buttonStyle := t.S().Text
-	
+
 	// Apply selection styling
 	if opts.Selected {
 		buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
 	} else {
 		buttonStyle = buttonStyle.Background(t.BgSubtle)
 	}
-	
+
 	// Create the button text with underlined character
 	text := opts.Text
 	if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
 		before := text[:opts.UnderlineIndex]
 		underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
 		after := text[opts.UnderlineIndex+1:]
-		
-		message := buttonStyle.Render(before) + 
-			buttonStyle.Underline(true).Render(underlined) + 
+
+		message := buttonStyle.Render(before) +
+			buttonStyle.Underline(true).Render(underlined) +
 			buttonStyle.Render(after)
-		
+
 		return buttonStyle.Padding(0, 2).Render(message)
 	}
-	
+
 	// Fallback if no underline index specified
 	return buttonStyle.Padding(0, 2).Render(text)
 }
@@ -120,7 +120,7 @@ func SelectableButtons(buttons []ButtonOpts, spacing string) string {
 	if spacing == "" {
 		spacing = "  "
 	}
-	
+
 	var parts []string
 	for i, button := range buttons {
 		parts = append(parts, SelectableButton(button))
@@ -128,6 +128,6 @@ func SelectableButtons(buttons []ButtonOpts, spacing string) string {
 			parts = append(parts, spacing)
 		}
 	}
-	
+
 	return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
 }

internal/tui/components/logs/details.go 🔗

@@ -52,43 +52,55 @@ func (i *detailCmp) updateContent() {
 	var content strings.Builder
 	t := styles.CurrentTheme()
 
-	// Format the header with timestamp and level
-	timeStyle := t.S().Muted
+	if i.currentLog.ID == "" {
+		content.WriteString(t.S().Muted.Render("No log selected"))
+		i.viewport.SetContent(content.String())
+		return
+	}
+
+	// Level badge with background color
 	levelStyle := getLevelStyle(i.currentLog.Level)
+	levelBadge := levelStyle.Padding(0, 1).Render(strings.ToUpper(i.currentLog.Level))
 
+	// Timestamp with relative time
+	timeStr := i.currentLog.Time.Format("2006-01-05 15:04:05 UTC")
+	relativeTime := getRelativeTime(i.currentLog.Time)
+	timeStyle := t.S().Muted
+
+	// Header line
 	header := lipgloss.JoinHorizontal(
-		lipgloss.Center,
-		timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
-		"  ",
-		levelStyle.Render(i.currentLog.Level),
+		lipgloss.Left,
+		timeStr,
+		" ",
+		timeStyle.Render(relativeTime),
 	)
 
-	content.WriteString(lipgloss.NewStyle().Bold(true).Render(header))
+	content.WriteString(levelBadge)
+	content.WriteString("\n\n")
+	content.WriteString(header)
 	content.WriteString("\n\n")
 
-	// Message with styling
-	messageStyle := t.S().Text.Bold(true)
-	content.WriteString(messageStyle.Render("Message:"))
+	// Message section
+	messageHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
+	content.WriteString(messageHeaderStyle.Render("Message"))
 	content.WriteString("\n")
-	content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
+	content.WriteString(i.currentLog.Message)
 	content.WriteString("\n\n")
 
 	// Attributes section
 	if len(i.currentLog.Attributes) > 0 {
-		attrHeaderStyle := t.S().Text.Bold(true)
-		content.WriteString(attrHeaderStyle.Render("Attributes:"))
+		attrHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
+		content.WriteString(attrHeaderStyle.Render("Attributes"))
 		content.WriteString("\n")
 
-		// Create a table-like display for attributes
-		keyStyle := t.S().Base.Foreground(t.Primary).Bold(true)
-		valueStyle := t.S().Text
-
 		for _, attr := range i.currentLog.Attributes {
+			keyStyle := t.S().Base.Foreground(t.Accent)
+			valueStyle := t.S().Text
 			attrLine := fmt.Sprintf("%s: %s",
 				keyStyle.Render(attr.Key),
 				valueStyle.Render(attr.Value),
 			)
-			content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine))
+			content.WriteString(attrLine)
 			content.WriteString("\n")
 		}
 	}
@@ -102,20 +114,48 @@ func getLevelStyle(level string) lipgloss.Style {
 
 	switch strings.ToLower(level) {
 	case "info":
-		return style.Foreground(t.Info)
+		return style.Foreground(t.White).Background(t.Info)
 	case "warn", "warning":
-		return style.Foreground(t.Warning)
+		return style.Foreground(t.White).Background(t.Warning)
 	case "error", "err":
-		return style.Foreground(t.Error)
+		return style.Foreground(t.White).Background(t.Error)
 	case "debug":
-		return style.Foreground(t.Success)
+		return style.Foreground(t.White).Background(t.Success)
+	case "fatal":
+		return style.Foreground(t.White).Background(t.Error)
 	default:
 		return style.Foreground(t.FgBase)
 	}
 }
 
+func getRelativeTime(logTime time.Time) string {
+	now := time.Now()
+	diff := now.Sub(logTime)
+
+	if diff < time.Minute {
+		return fmt.Sprintf("%ds ago", int(diff.Seconds()))
+	} else if diff < time.Hour {
+		return fmt.Sprintf("%dm ago", int(diff.Minutes()))
+	} else if diff < 24*time.Hour {
+		return fmt.Sprintf("%dh ago", int(diff.Hours()))
+	} else if diff < 30*24*time.Hour {
+		return fmt.Sprintf("%dd ago", int(diff.Hours()/24))
+	} else if diff < 365*24*time.Hour {
+		return fmt.Sprintf("%dmo ago", int(diff.Hours()/(24*30)))
+	} else {
+		return fmt.Sprintf("%dy ago", int(diff.Hours()/(24*365)))
+	}
+}
+
 func (i *detailCmp) View() tea.View {
-	return tea.NewView(i.viewport.View())
+	t := styles.CurrentTheme()
+	style := t.S().Base.
+		BorderStyle(lipgloss.RoundedBorder()).
+		BorderForeground(t.BorderFocus).
+		Width(i.width - 2).   // Adjust width for border
+		Height(i.height - 2). // Adjust height for border
+		Padding(1)
+	return tea.NewView(style.Render(i.viewport.View()))
 }
 
 func (i *detailCmp) GetSize() (int, int) {
@@ -123,10 +163,11 @@ func (i *detailCmp) GetSize() (int, int) {
 }
 
 func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
+	logging.Info("Setting size for detail component", "width", width, "height", height)
 	i.width = width
 	i.height = height
-	i.viewport.SetWidth(i.width)
-	i.viewport.SetHeight(i.height)
+	i.viewport.SetWidth(i.width - 4)
+	i.viewport.SetHeight(i.height - 4)
 	i.updateContent()
 	return nil
 }

internal/tui/components/logs/table.go 🔗

@@ -1,8 +1,9 @@
 package logs
 
 import (
-	"encoding/json"
+	"fmt"
 	"slices"
+	"strings"
 
 	"github.com/charmbracelet/bubbles/v2/key"
 	"github.com/charmbracelet/bubbles/v2/table"
@@ -12,6 +13,7 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/layout"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
+	"github.com/charmbracelet/lipgloss/v2"
 )
 
 type TableComponent interface {
@@ -22,48 +24,80 @@ type TableComponent interface {
 
 type tableCmp struct {
 	table table.Model
+	logs  []logging.LogMessage
 }
 
 type selectedLogMsg logging.LogMessage
 
 func (i *tableCmp) Init() tea.Cmd {
+	i.logs = logging.List()
 	i.setRows()
 	return nil
 }
 
 func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
-	switch msg.(type) {
+	switch msg := msg.(type) {
 	case pubsub.Event[logging.LogMessage]:
-		i.setRows()
-		return i, nil
+		return i, func() tea.Msg {
+			if msg.Type == pubsub.CreatedEvent {
+				rows := i.table.Rows()
+				for _, row := range rows {
+					if row[1] == msg.Payload.ID {
+						return nil // If the log already exists, do not add it again
+					}
+				}
+				i.logs = append(i.logs, msg.Payload)
+				i.table.SetRows(
+					append(
+						[]table.Row{
+							logToRow(msg.Payload),
+						},
+						i.table.Rows()...,
+					),
+				)
+			}
+			return selectedLogMsg(msg.Payload)
+		}
 	}
-	prevSelectedRow := i.table.SelectedRow()
 	t, cmd := i.table.Update(msg)
 	cmds = append(cmds, cmd)
 	i.table = t
-	selectedRow := i.table.SelectedRow()
-	if selectedRow != nil {
-		if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] {
-			var log logging.LogMessage
-			for _, row := range logging.List() {
-				if row.ID == selectedRow[0] {
-					log = row
-					break
-				}
-			}
-			if log.ID != "" {
-				cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
+
+	cmds = append(cmds, func() tea.Msg {
+		for _, log := range logging.List() {
+			if log.ID == i.table.SelectedRow()[1] {
+				// If the selected row matches the log ID, return the selected log message
+				return selectedLogMsg(log)
 			}
 		}
-	}
+		return nil
+	})
 	return i, tea.Batch(cmds...)
 }
 
 func (i *tableCmp) View() tea.View {
 	t := styles.CurrentTheme()
 	defaultStyles := table.DefaultStyles()
-	defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary)
+
+	// Header styling
+	defaultStyles.Header = defaultStyles.Header.
+		Foreground(t.Primary).
+		Bold(true).
+		BorderStyle(lipgloss.NormalBorder()).
+		BorderBottom(true).
+		BorderForeground(t.Border)
+
+	// Selected row styling
+	defaultStyles.Selected = defaultStyles.Selected.
+		Foreground(t.FgSelected).
+		Background(t.Primary).
+		Bold(false)
+
+	// Cell styling
+	defaultStyles.Cell = defaultStyles.Cell.
+		Foreground(t.FgBase)
+
 	i.table.SetStyles(defaultStyles)
 	return tea.NewView(i.table.View())
 }
@@ -75,12 +109,30 @@ func (i *tableCmp) GetSize() (int, int) {
 func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
 	i.table.SetWidth(width)
 	i.table.SetHeight(height)
-	cloumns := i.table.Columns()
-	for i, col := range cloumns {
-		col.Width = (width / len(cloumns)) - 2
-		cloumns[i] = col
-	}
-	i.table.SetColumns(cloumns)
+
+	columnWidth := (width - 10) / 4
+	i.table.SetColumns([]table.Column{
+		{
+			Title: "Level",
+			Width: 10,
+		},
+		{
+			Title: "ID",
+			Width: columnWidth,
+		},
+		{
+			Title: "Time",
+			Width: columnWidth,
+		},
+		{
+			Title: "Message",
+			Width: columnWidth,
+		},
+		{
+			Title: "Attributes",
+			Width: columnWidth,
+		},
+	})
 	return nil
 }
 
@@ -91,39 +143,54 @@ func (i *tableCmp) BindingKeys() []key.Binding {
 func (i *tableCmp) setRows() {
 	rows := []table.Row{}
 
-	logs := logging.List()
-	slices.SortFunc(logs, func(a, b logging.LogMessage) int {
+	slices.SortFunc(i.logs, func(a, b logging.LogMessage) int {
 		if a.Time.Before(b.Time) {
-			return 1
+			return -1
 		}
 		if a.Time.After(b.Time) {
-			return -1
+			return 1
 		}
 		return 0
 	})
 
-	for _, log := range logs {
-		bm, _ := json.Marshal(log.Attributes)
+	for _, log := range i.logs {
+		rows = append(rows, logToRow(log))
+	}
+	i.table.SetRows(rows)
+}
 
-		row := table.Row{
-			log.ID,
-			log.Time.Format("15:04:05"),
-			log.Level,
-			log.Message,
-			string(bm),
+func logToRow(log logging.LogMessage) table.Row {
+	// Format attributes as JSON string
+	var attrStr string
+	if len(log.Attributes) > 0 {
+		var parts []string
+		for _, attr := range log.Attributes {
+			parts = append(parts, fmt.Sprintf(`{"Key":"%s","Value":"%s"}`, attr.Key, attr.Value))
 		}
-		rows = append(rows, row)
+		attrStr = "[" + strings.Join(parts, ",") + "]"
+	}
+
+	// Format time with relative time
+	timeStr := log.Time.Format("2006-01-05 15:04:05 UTC")
+	relativeTime := getRelativeTime(log.Time)
+	fullTimeStr := timeStr + " " + relativeTime
+
+	return table.Row{
+		strings.ToUpper(log.Level),
+		log.ID,
+		fullTimeStr,
+		log.Message,
+		attrStr,
 	}
-	i.table.SetRows(rows)
 }
 
 func NewLogsTable() TableComponent {
 	columns := []table.Column{
-		{Title: "ID", Width: 4},
-		{Title: "Time", Width: 4},
-		{Title: "Level", Width: 10},
-		{Title: "Message", Width: 10},
-		{Title: "Attributes", Width: 10},
+		{Title: "Level"},
+		{Title: "ID"},
+		{Title: "Time"},
+		{Title: "Message"},
+		{Title: "Attributes"},
 	}
 
 	tableModel := table.New(

internal/tui/page/chat/chat.go 🔗

@@ -8,7 +8,6 @@ import (
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/llm/models"
-	"github.com/charmbracelet/crush/internal/logging"
 	"github.com/charmbracelet/crush/internal/message"
 	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -99,7 +98,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				return p, util.ReportWarn("File attachments are not supported by the current model: " + string(selectedModelID))
 			}
 		case key.Matches(msg, p.keyMap.Tab):
-			logging.Info("Tab key pressed, toggling chat focus")
 			if p.session.ID == "" {
 				return p, nil
 			}

internal/tui/page/logs/keys.go 🔗

@@ -0,0 +1,37 @@
+package logs
+
+import (
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/crush/internal/tui/layout"
+)
+
+type KeyMap struct {
+	Back key.Binding
+}
+
+func DefaultKeyMap() KeyMap {
+	return KeyMap{
+		Back: key.NewBinding(
+			key.WithKeys("esc", "backspace"),
+			key.WithHelp("esc/backspace", "back to chat"),
+		),
+	}
+}
+
+// FullHelp implements help.KeyMap.
+func (k KeyMap) FullHelp() [][]key.Binding {
+	m := [][]key.Binding{}
+	slice := layout.KeyMapToSlice(k)
+	for i := 0; i < len(slice); i += 4 {
+		end := min(i+4, len(slice))
+		m = append(m, slice[i:end])
+	}
+	return m
+}
+
+// ShortHelp implements help.KeyMap.
+func (k KeyMap) ShortHelp() []key.Binding {
+	return []key.Binding{
+		k.Back,
+	}
+}

internal/tui/page/logs.go → internal/tui/page/logs/logs.go 🔗

@@ -1,26 +1,30 @@
-package page
+package logs
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/crush/internal/tui/components/logs"
+	"github.com/charmbracelet/crush/internal/tui/components/core"
+	logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs"
 	"github.com/charmbracelet/crush/internal/tui/layout"
+	"github.com/charmbracelet/crush/internal/tui/page"
+	"github.com/charmbracelet/crush/internal/tui/page/chat"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/lipgloss/v2"
 )
 
-var LogsPage PageID = "logs"
+var LogsPage page.PageID = "logs"
 
 type LogPage interface {
 	util.Model
 	layout.Sizeable
-	layout.Bindings
 }
+
 type logsPage struct {
 	width, height int
-	table         layout.Container
-	details       layout.Container
+	table         logsComponents.TableComponent
+	details       logsComponents.DetailComponent
+	keyMap        KeyMap
 }
 
 func (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) {
 		p.width = msg.Width
 		p.height = msg.Height
 		return p, p.SetSize(msg.Width, msg.Height)
+	case tea.KeyMsg:
+		switch {
+		case key.Matches(msg, p.keyMap.Back):
+			return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPage})
+		}
 	}
 
 	table, cmd := p.table.Update(msg)
 	cmds = append(cmds, cmd)
-	p.table = table.(layout.Container)
+	p.table = table.(logsComponents.TableComponent)
 	details, cmd := p.details.Update(msg)
 	cmds = append(cmds, cmd)
-	p.details = details.(layout.Container)
+	p.details = details.(logsComponents.DetailComponent)
 
 	return p, tea.Batch(cmds...)
 }
 
 func (p *logsPage) View() tea.View {
-	style := styles.CurrentTheme().S().Base.Width(p.width).Height(p.height)
+	baseStyle := styles.CurrentTheme().S().Base
+	style := baseStyle.Width(p.width).Height(p.height).Padding(1)
+	title := core.Title("Logs", p.width-2)
+
 	return tea.NewView(
 		style.Render(
 			lipgloss.JoinVertical(lipgloss.Top,
-				p.table.View().String(),
+				title,
 				p.details.View().String(),
+				p.table.View().String(),
 			),
 		),
 	)
 }
 
-func (p *logsPage) BindingKeys() []key.Binding {
-	return p.table.BindingKeys()
-}
-
 // GetSize implements LogPage.
 func (p *logsPage) GetSize() (int, int) {
 	return p.width, p.height
@@ -67,9 +76,11 @@ func (p *logsPage) GetSize() (int, int) {
 func (p *logsPage) SetSize(width int, height int) tea.Cmd {
 	p.width = width
 	p.height = height
+	availableHeight := height - 2 // Padding for top and bottom
+	availableHeight -= 1          // title height
 	return tea.Batch(
-		p.table.SetSize(width, height/2),
-		p.details.SetSize(width, height/2),
+		p.table.SetSize(width-2, availableHeight/2),
+		p.details.SetSize(width-2, availableHeight/2),
 	)
 }
 
@@ -82,7 +93,8 @@ func (p *logsPage) Init() tea.Cmd {
 
 func NewLogsPage() LogPage {
 	return &logsPage{
-		table:   layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll()),
-		details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll()),
+		details: logsComponents.NewLogsDetails(),
+		table:   logsComponents.NewLogsTable(),
+		keyMap:  DefaultKeyMap(),
 	}
 }

internal/tui/tui.go 🔗

@@ -27,6 +27,7 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/layout"
 	"github.com/charmbracelet/crush/internal/tui/page"
 	"github.com/charmbracelet/crush/internal/tui/page/chat"
+	"github.com/charmbracelet/crush/internal/tui/page/logs"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/lipgloss/v2"
@@ -137,7 +138,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		cmds = append(cmds, statusCmd)
 
 		// If the current page is logs, update the logs view
-		if a.currentPage == page.LogsPage {
+		if a.currentPage == logs.LogsPage {
 			updated, pageCmd := a.pages[a.currentPage].Update(msg)
 			a.pages[a.currentPage] = updated.(util.Model)
 			cmds = append(cmds, pageCmd)
@@ -328,7 +329,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
 		return tea.Sequence(cmds...)
 	// Page navigation
 	case key.Matches(msg, a.keyMap.Logs):
-		return a.moveToPage(page.LogsPage)
+		return a.moveToPage(logs.LogsPage)
 
 	default:
 		if a.dialog.HasDialogs() {
@@ -379,7 +380,6 @@ func (a *appModel) View() tea.View {
 		lipgloss.NewLayer(appView),
 	}
 	if a.dialog.HasDialogs() {
-		logging.Info("Rendering dialogs")
 		layers = append(
 			layers,
 			a.dialog.GetLayers()...,
@@ -424,7 +424,7 @@ func New(app *app.App) tea.Model {
 
 		pages: map[page.PageID]util.Model{
 			chat.ChatPage: chat.NewChatPage(app),
-			page.LogsPage: page.NewLogsPage(),
+			logs.LogsPage: logs.NewLogsPage(),
 		},
 
 		dialog:      dialogs.NewDialogCmp(),

todos.md 🔗

@@ -11,9 +11,9 @@
 
 - [x] Cleanup Commands
 - [x] Sessions dialog
-- [ ] Models
-- [~] Move sessions and model dialog to the commands
-- [ ] Add sessions shortuct
+- [x] Models
+- [x] Move sessions and model dialog to the commands
+- [x] Add sessions shortuct
 - [ ] Add all posible actions to the commands
 
 ## Investigate
@@ -24,3 +24,6 @@
 ## Messages
 
 - [ ] Fix issue with numbers (padding)
+- [ ] Run tools in parallel and add the responses in parallel
+- [ ] Handle parallel permission calls
+- [ ] Weird behavior sometimes the message does not update