on main help sign in
intiial layout
23e7a95Kujtim Hoxha
internal/tui/components/chat/editor.gonew file+62
@@ -0,0 +1,62 @@
1+package chat
2+
3+import (
4+ "github.com/charmbracelet/bubbles/key"
5+ "github.com/charmbracelet/bubbles/textarea"
6+ tea "github.com/charmbracelet/bubbletea"
7+ "github.com/charmbracelet/lipgloss"
8+ "github.com/kujtimiihoxha/termai/internal/tui/layout"
9+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
10+)
11+
12+type editorCmp struct {
13+ textarea textarea.Model
14+}
15+
16+func (m *editorCmp) Init() tea.Cmd {
17+ return textarea.Blink
18+}
19+
20+func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
21+ var cmd tea.Cmd
22+ m.textarea, cmd = m.textarea.Update(msg)
23+ return m, cmd
24+}
25+
26+func (m *editorCmp) View() string {
27+ style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
28+
29+ return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
30+}
31+
32+func (m *editorCmp) SetSize(width, height int) {
33+ m.textarea.SetWidth(width - 3) // account for the prompt and padding right
34+ m.textarea.SetHeight(height)
35+}
36+
37+func (m *editorCmp) GetSize() (int, int) {
38+ return m.textarea.Width(), m.textarea.Height()
39+}
40+
41+func (m *editorCmp) BindingKeys() []key.Binding {
42+ return layout.KeyMapToSlice(m.textarea.KeyMap)
43+}
44+
45+func NewEditorCmp() tea.Model {
46+ ti := textarea.New()
47+ ti.Prompt = " "
48+ ti.ShowLineNumbers = false
49+ ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background)
50+ ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background)
51+ ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background)
52+ ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
53+
54+ ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background)
55+ ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
56+ ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
57+ ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
58+ ti.Focus()
59+ return &editorCmp{
60+ textarea: ti,
61+ }
62+}
internal/tui/components/chat/messages.gonew file+21
@@ -0,0 +1,21 @@
1+package chat
2+
3+import tea "github.com/charmbracelet/bubbletea"
4+
5+type messagesCmp struct{}
6+
7+func (m *messagesCmp) Init() tea.Cmd {
8+ return nil
9+}
10+
11+func (m *messagesCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
12+ return m, nil
13+}
14+
15+func (m *messagesCmp) View() string {
16+ return "Messages"
17+}
18+
19+func NewMessagesCmp() tea.Model {
20+ return &messagesCmp{}
21+}
internal/tui/components/chat/sidebar.gonew file+21
@@ -0,0 +1,21 @@
1+package chat
2+
3+import tea "github.com/charmbracelet/bubbletea"
4+
5+type sidebarCmp struct{}
6+
7+func (m *sidebarCmp) Init() tea.Cmd {
8+ return nil
9+}
10+
11+func (m *sidebarCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
12+ return m, nil
13+}
14+
15+func (m *sidebarCmp) View() string {
16+ return "Sidebar"
17+}
18+
19+func NewSidebarCmp() tea.Model {
20+ return &sidebarCmp{}
21+}
internal/tui/components/dialog/permission.go+1−1
@@ -441,7 +441,7 @@ func NewPermissionDialogCmd(permission permission.PermissionRequest) tea.Cmd {
441 layout.WithSinglePaneBordered(true),
441 layout.WithSinglePaneBordered(true),
442 layout.WithSinglePaneFocusable(true),
442 layout.WithSinglePaneFocusable(true),
443 layout.WithSinglePaneActiveColor(styles.Warning),
443 layout.WithSinglePaneActiveColor(styles.Warning),
444− layout.WithSignlePaneBorderText(map[layout.BorderPosition]string{
444+ layout.WithSinglePaneBorderText(map[layout.BorderPosition]string{
445 layout.TopMiddleBorder: " Permission Required ",
445 layout.TopMiddleBorder: " Permission Required ",
446 }),
446 }),
447 )
447 )
internal/tui/components/repl/editor.go+27−21
@@ -156,30 +156,36 @@ func (m *editorCmp) Cancel() tea.Cmd {
156}
156}
157
157
158func (m *editorCmp) Send() tea.Cmd {
158func (m *editorCmp) Send() tea.Cmd {
159− return func() tea.Msg {
159+ if m.cancelMessage != nil {
160− messages, err := m.app.Messages.List(m.sessionID)
161− if err != nil {
162− return util.ReportError(err)
163− }
164− if hasUnfinishedMessages(messages) {
165− return util.ReportWarn("Assistant is still working on the previous message")
160+ return util.ReportWarn("Assistant is still working on the previous message")
166− }
167− a, err := agent.NewCoderAgent(m.app)
168− if err != nil {
169− return util.ReportError(err)
170− }
161+ }
171
162
172− content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
163+ messages, err := m.app.Messages.List(m.sessionID)
173− ctx, cancel := context.WithCancel(m.app.Context)
164+ if err != nil {
174− m.cancelMessage = cancel
165+ return util.ReportError(err)
175− go func() {
166+ }
176− defer cancel()
167+ if hasUnfinishedMessages(messages) {
177− a.Generate(ctx, m.sessionID, content)
168+ return util.ReportWarn("Assistant is still working on the previous message")
178− m.cancelMessage = nil
169+ }
179− }()
170+
171+ a, err := agent.NewCoderAgent(m.app)
172+ if err != nil {
173+ return util.ReportError(err)
174+ }
180
175
181− return m.editor.Reset()
176+ content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
177+ if len(content) == 0 {
178+ return util.ReportWarn("Message is empty")
182 }
179 }
180+ ctx, cancel := context.WithCancel(m.app.Context)
181+ m.cancelMessage = cancel
182+ go func() {
183+ defer cancel()
184+ a.Generate(ctx, m.sessionID, content)
185+ m.cancelMessage = nil
186+ }()
187+
188+ return m.editor.Reset()
183}
189}
184
190
185func (m *editorCmp) View() string {
191func (m *editorCmp) View() string {
internal/tui/layout/container.gonew file+224
@@ -0,0 +1,224 @@
1+package layout
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+type Container interface {
11+ tea.Model
12+ Sizeable
13+}
14+type container struct {
15+ width int
16+ height int
17+
18+ content tea.Model
19+
20+ // Style options
21+ paddingTop int
22+ paddingRight int
23+ paddingBottom int
24+ paddingLeft int
25+
26+ borderTop bool
27+ borderRight bool
28+ borderBottom bool
29+ borderLeft bool
30+ borderStyle lipgloss.Border
31+ borderColor lipgloss.TerminalColor
32+
33+ backgroundColor lipgloss.TerminalColor
34+}
35+
36+func (c *container) Init() tea.Cmd {
37+ return c.content.Init()
38+}
39+
40+func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
41+ u, cmd := c.content.Update(msg)
42+ c.content = u
43+ return c, cmd
44+}
45+
46+func (c *container) View() string {
47+ style := lipgloss.NewStyle()
48+ width := c.width
49+ height := c.height
50+ // Apply background color if specified
51+ if c.backgroundColor != nil {
52+ style = style.Background(c.backgroundColor)
53+ }
54+
55+ // Apply border if any side is enabled
56+ if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
57+ // Adjust width and height for borders
58+ if c.borderTop {
59+ height--
60+ }
61+ if c.borderBottom {
62+ height--
63+ }
64+ if c.borderLeft {
65+ width--
66+ }
67+ if c.borderRight {
68+ width--
69+ }
70+ style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
71+
72+ // Apply border color if specified
73+ if c.borderColor != nil {
74+ style = style.BorderBackground(c.backgroundColor).BorderForeground(c.borderColor)
75+ }
76+ }
77+ style = style.
78+ Width(width).
79+ Height(height).
80+ PaddingTop(c.paddingTop).
81+ PaddingRight(c.paddingRight).
82+ PaddingBottom(c.paddingBottom).
83+ PaddingLeft(c.paddingLeft)
84+
85+ return style.Render(c.content.View())
86+}
87+
88+func (c *container) SetSize(width, height int) {
89+ c.width = width
90+ c.height = height
91+
92+ // If the content implements Sizeable, adjust its size to account for padding and borders
93+ if sizeable, ok := c.content.(Sizeable); ok {
94+ // Calculate horizontal space taken by padding and borders
95+ horizontalSpace := c.paddingLeft + c.paddingRight
96+ if c.borderLeft {
97+ horizontalSpace++
98+ }
99+ if c.borderRight {
100+ horizontalSpace++
101+ }
102+
103+ // Calculate vertical space taken by padding and borders
104+ verticalSpace := c.paddingTop + c.paddingBottom
105+ if c.borderTop {
106+ verticalSpace++
107+ }
108+ if c.borderBottom {
109+ verticalSpace++
110+ }
111+
112+ // Set content size with adjusted dimensions
113+ contentWidth := max(0, width-horizontalSpace)
114+ contentHeight := max(0, height-verticalSpace)
115+ sizeable.SetSize(contentWidth, contentHeight)
116+ }
117+}
118+
119+func (c *container) GetSize() (int, int) {
120+ return c.width, c.height
121+}
122+
123+func (c *container) BindingKeys() []key.Binding {
124+ if b, ok := c.content.(Bindings); ok {
125+ return b.BindingKeys()
126+ }
127+ return []key.Binding{}
128+}
129+
130+type ContainerOption func(*container)
131+
132+func NewContainer(content tea.Model, options ...ContainerOption) Container {
133+ c := &container{
134+ content: content,
135+ borderColor: styles.BorderColor,
136+ borderStyle: lipgloss.NormalBorder(),
137+ backgroundColor: styles.Background,
138+ }
139+
140+ for _, option := range options {
141+ option(c)
142+ }
143+
144+ return c
145+}
146+
147+// Padding options
148+func WithPadding(top, right, bottom, left int) ContainerOption {
149+ return func(c *container) {
150+ c.paddingTop = top
151+ c.paddingRight = right
152+ c.paddingBottom = bottom
153+ c.paddingLeft = left
154+ }
155+}
156+
157+func WithPaddingAll(padding int) ContainerOption {
158+ return WithPadding(padding, padding, padding, padding)
159+}
160+
161+func WithPaddingHorizontal(padding int) ContainerOption {
162+ return func(c *container) {
163+ c.paddingLeft = padding
164+ c.paddingRight = padding
165+ }
166+}
167+
168+func WithPaddingVertical(padding int) ContainerOption {
169+ return func(c *container) {
170+ c.paddingTop = padding
171+ c.paddingBottom = padding
172+ }
173+}
174+
175+func WithBorder(top, right, bottom, left bool) ContainerOption {
176+ return func(c *container) {
177+ c.borderTop = top
178+ c.borderRight = right
179+ c.borderBottom = bottom
180+ c.borderLeft = left
181+ }
182+}
183+
184+func WithBorderAll() ContainerOption {
185+ return WithBorder(true, true, true, true)
186+}
187+
188+func WithBorderHorizontal() ContainerOption {
189+ return WithBorder(true, false, true, false)
190+}
191+
192+func WithBorderVertical() ContainerOption {
193+ return WithBorder(false, true, false, true)
194+}
195+
196+func WithBorderStyle(style lipgloss.Border) ContainerOption {
197+ return func(c *container) {
198+ c.borderStyle = style
199+ }
200+}
201+
202+func WithBorderColor(color lipgloss.TerminalColor) ContainerOption {
203+ return func(c *container) {
204+ c.borderColor = color
205+ }
206+}
207+
208+func WithRoundedBorder() ContainerOption {
209+ return WithBorderStyle(lipgloss.RoundedBorder())
210+}
211+
212+func WithThickBorder() ContainerOption {
213+ return WithBorderStyle(lipgloss.ThickBorder())
214+}
215+
216+func WithDoubleBorder() ContainerOption {
217+ return WithBorderStyle(lipgloss.DoubleBorder())
218+}
219+
220+func WithBackgroundColor(color lipgloss.TerminalColor) ContainerOption {
221+ return func(c *container) {
222+ c.backgroundColor = color
223+ }
224+}
internal/tui/layout/single.go+2−2
@@ -151,7 +151,7 @@ func NewSinglePane(content tea.Model, opts ...SinglePaneOption) SinglePaneLayout
151 return layout
151 return layout
152}
152}
153
153
154−func WithSignlePaneSize(width, height int) SinglePaneOption {
154+func WithSinglePaneSize(width, height int) SinglePaneOption {
155 return func(opts *singlePaneLayout) {
155 return func(opts *singlePaneLayout) {
156 opts.width = width
156 opts.width = width
157 opts.height = height
157 opts.height = height
@@ -170,7 +170,7 @@ func WithSinglePaneBordered(bordered bool) SinglePaneOption {
170 }
170 }
171}
171}
172
172
173−func WithSignlePaneBorderText(borderText map[BorderPosition]string) SinglePaneOption {
173+func WithSinglePaneBorderText(borderText map[BorderPosition]string) SinglePaneOption {
174 return func(opts *singlePaneLayout) {
174 return func(opts *singlePaneLayout) {
175 opts.borderText = borderText
175 opts.borderText = borderText
176 }
176 }
internal/tui/layout/split.gonew file+229
@@ -0,0 +1,229 @@
1+package layout
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+type SplitPaneLayout interface {
11+ tea.Model
12+ Sizeable
13+}
14+
15+type splitPaneLayout struct {
16+ width int
17+ height int
18+ ratio float64
19+ verticalRatio float64
20+
21+ rightPanel Container
22+ leftPanel Container
23+ bottomPanel Container
24+
25+ backgroundColor lipgloss.TerminalColor
26+}
27+
28+type SplitPaneOption func(*splitPaneLayout)
29+
30+func (s *splitPaneLayout) Init() tea.Cmd {
31+ var cmds []tea.Cmd
32+
33+ if s.leftPanel != nil {
34+ cmds = append(cmds, s.leftPanel.Init())
35+ }
36+
37+ if s.rightPanel != nil {
38+ cmds = append(cmds, s.rightPanel.Init())
39+ }
40+
41+ if s.bottomPanel != nil {
42+ cmds = append(cmds, s.bottomPanel.Init())
43+ }
44+
45+ return tea.Batch(cmds...)
46+}
47+
48+func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
49+ var cmds []tea.Cmd
50+ switch msg := msg.(type) {
51+ case tea.WindowSizeMsg:
52+ s.SetSize(msg.Width, msg.Height)
53+ return s, nil
54+ }
55+
56+ if s.rightPanel != nil {
57+ u, cmd := s.rightPanel.Update(msg)
58+ s.rightPanel = u.(Container)
59+ if cmd != nil {
60+ cmds = append(cmds, cmd)
61+ }
62+ }
63+
64+ if s.leftPanel != nil {
65+ u, cmd := s.leftPanel.Update(msg)
66+ s.leftPanel = u.(Container)
67+ if cmd != nil {
68+ cmds = append(cmds, cmd)
69+ }
70+ }
71+
72+ if s.bottomPanel != nil {
73+ u, cmd := s.bottomPanel.Update(msg)
74+ s.bottomPanel = u.(Container)
75+ if cmd != nil {
76+ cmds = append(cmds, cmd)
77+ }
78+ }
79+
80+ return s, tea.Batch(cmds...)
81+}
82+
83+func (s *splitPaneLayout) View() string {
84+ var topSection string
85+
86+ if s.leftPanel != nil && s.rightPanel != nil {
87+ leftView := s.leftPanel.View()
88+ rightView := s.rightPanel.View()
89+ topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
90+ } else if s.leftPanel != nil {
91+ topSection = s.leftPanel.View()
92+ } else if s.rightPanel != nil {
93+ topSection = s.rightPanel.View()
94+ } else {
95+ topSection = ""
96+ }
97+
98+ var finalView string
99+
100+ if s.bottomPanel != nil && topSection != "" {
101+ bottomView := s.bottomPanel.View()
102+ finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
103+ } else if s.bottomPanel != nil {
104+ finalView = s.bottomPanel.View()
105+ } else {
106+ finalView = topSection
107+ }
108+
109+ if s.backgroundColor != nil && finalView != "" {
110+ style := lipgloss.NewStyle().
111+ Width(s.width).
112+ Height(s.height).
113+ Background(s.backgroundColor)
114+
115+ return style.Render(finalView)
116+ }
117+
118+ return finalView
119+}
120+
121+func (s *splitPaneLayout) SetSize(width, height int) {
122+ s.width = width
123+ s.height = height
124+
125+ var topHeight, bottomHeight int
126+ if s.bottomPanel != nil {
127+ topHeight = int(float64(height) * s.verticalRatio)
128+ bottomHeight = height - topHeight
129+ } else {
130+ topHeight = height
131+ bottomHeight = 0
132+ }
133+
134+ var leftWidth, rightWidth int
135+ if s.leftPanel != nil && s.rightPanel != nil {
136+ leftWidth = int(float64(width) * s.ratio)
137+ rightWidth = width - leftWidth
138+ } else if s.leftPanel != nil {
139+ leftWidth = width
140+ rightWidth = 0
141+ } else if s.rightPanel != nil {
142+ leftWidth = 0
143+ rightWidth = width
144+ }
145+
146+ if s.leftPanel != nil {
147+ s.leftPanel.SetSize(leftWidth, topHeight)
148+ }
149+
150+ if s.rightPanel != nil {
151+ s.rightPanel.SetSize(rightWidth, topHeight)
152+ }
153+
154+ if s.bottomPanel != nil {
155+ s.bottomPanel.SetSize(width, bottomHeight)
156+ }
157+}
158+
159+func (s *splitPaneLayout) GetSize() (int, int) {
160+ return s.width, s.height
161+}
162+
163+func (s *splitPaneLayout) BindingKeys() []key.Binding {
164+ keys := []key.Binding{}
165+ if s.leftPanel != nil {
166+ if b, ok := s.leftPanel.(Bindings); ok {
167+ keys = append(keys, b.BindingKeys()...)
168+ }
169+ }
170+ if s.rightPanel != nil {
171+ if b, ok := s.rightPanel.(Bindings); ok {
172+ keys = append(keys, b.BindingKeys()...)
173+ }
174+ }
175+ if s.bottomPanel != nil {
176+ if b, ok := s.bottomPanel.(Bindings); ok {
177+ keys = append(keys, b.BindingKeys()...)
178+ }
179+ }
180+ return keys
181+}
182+
183+func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
184+ layout := &splitPaneLayout{
185+ ratio: 0.7,
186+ verticalRatio: 0.9, // Default 80% for top section, 20% for bottom
187+ backgroundColor: styles.Background,
188+ }
189+ for _, option := range options {
190+ option(layout)
191+ }
192+ return layout
193+}
194+
195+func WithLeftPanel(panel Container) SplitPaneOption {
196+ return func(s *splitPaneLayout) {
197+ s.leftPanel = panel
198+ }
199+}
200+
201+func WithRightPanel(panel Container) SplitPaneOption {
202+ return func(s *splitPaneLayout) {
203+ s.rightPanel = panel
204+ }
205+}
206+
207+func WithRatio(ratio float64) SplitPaneOption {
208+ return func(s *splitPaneLayout) {
209+ s.ratio = ratio
210+ }
211+}
212+
213+func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption {
214+ return func(s *splitPaneLayout) {
215+ s.backgroundColor = color
216+ }
217+}
218+
219+func WithBottomPanel(panel Container) SplitPaneOption {
220+ return func(s *splitPaneLayout) {
221+ s.bottomPanel = panel
222+ }
223+}
224+
225+func WithVerticalRatio(ratio float64) SplitPaneOption {
226+ return func(s *splitPaneLayout) {
227+ s.verticalRatio = ratio
228+ }
229+}
internal/tui/page/chat.gonew file+30
@@ -0,0 +1,30 @@
1+package page
2+
3+import (
4+ tea "github.com/charmbracelet/bubbletea"
5+ "github.com/kujtimiihoxha/termai/internal/app"
6+ "github.com/kujtimiihoxha/termai/internal/tui/components/chat"
7+ "github.com/kujtimiihoxha/termai/internal/tui/layout"
8+)
9+
10+var ChatPage PageID = "chat"
11+
12+func NewChatPage(app *app.App) tea.Model {
13+ messagesContainer := layout.NewContainer(
14+ chat.NewMessagesCmp(),
15+ layout.WithPadding(1, 1, 1, 1),
16+ )
17+ sidebarContainer := layout.NewContainer(
18+ chat.NewSidebarCmp(),
19+ layout.WithPadding(1, 1, 1, 1),
20+ )
21+ editorContainer := layout.NewContainer(
22+ chat.NewEditorCmp(),
23+ layout.WithBorder(true, false, false, false),
24+ )
25+ return layout.NewSplitPane(
26+ layout.WithRightPanel(sidebarContainer),
27+ layout.WithLeftPanel(messagesContainer),
28+ layout.WithBottomPanel(editorContainer),
29+ )
30+}
internal/tui/page/init.go+1−1
@@ -299,7 +299,7 @@ func NewInitPage() tea.Model {
299 initModel,
299 initModel,
300 layout.WithSinglePaneFocusable(true),
300 layout.WithSinglePaneFocusable(true),
301 layout.WithSinglePaneBordered(true),
301 layout.WithSinglePaneBordered(true),
302− layout.WithSignlePaneBorderText(
302+ layout.WithSinglePaneBorderText(
303 map[layout.BorderPosition]string{
303 map[layout.BorderPosition]string{
304 layout.TopMiddleBorder: "Welcome to termai - Initial Setup",
304 layout.TopMiddleBorder: "Welcome to termai - Initial Setup",
305 },
305 },
internal/tui/styles/styles.go+16
@@ -10,6 +10,22 @@ var (
10 dark = catppuccin.Mocha
10 dark = catppuccin.Mocha
11)
11)
12
12
13+// NEW STYLES
14+var (
15+ Background = lipgloss.AdaptiveColor{
16+ Dark: "#212121",
17+ Light: "#212121",
18+ }
19+ BackgroundDarker = lipgloss.AdaptiveColor{
20+ Dark: "#181818",
21+ Light: "#181818",
22+ }
23+ BorderColor = lipgloss.AdaptiveColor{
24+ Dark: "#4b4c5c",
25+ Light: "#4b4c5c",
26+ }
27+)
28+
13var (
29var (
14 Regular = lipgloss.NewStyle()
30 Regular = lipgloss.NewStyle()
15 Bold = Regular.Bold(true)
31 Bold = Regular.Bold(true)
internal/tui/tui.go+4−1
@@ -200,6 +200,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
200 }
200 }
201 case key.Matches(msg, keys.Logs):
201 case key.Matches(msg, keys.Logs):
202 return a, a.moveToPage(page.LogsPage)
202 return a, a.moveToPage(page.LogsPage)
203+ case msg.String() == "O":
204+ return a, a.moveToPage(page.ReplPage)
203 case key.Matches(msg, keys.Help):
205 case key.Matches(msg, keys.Help):
204 a.ToggleHelp()
206 a.ToggleHelp()
205 return a, nil
207 return a, nil
@@ -292,7 +294,7 @@ func New(app *app.App) tea.Model {
292 // homedir, _ := os.UserHomeDir()
294 // homedir, _ := os.UserHomeDir()
293 // configPath := filepath.Join(homedir, ".termai.yaml")
295 // configPath := filepath.Join(homedir, ".termai.yaml")
294 //
296 //
295− startPage := page.ReplPage
297+ startPage := page.ChatPage
296 // if _, err := os.Stat(configPath); os.IsNotExist(err) {
298 // if _, err := os.Stat(configPath); os.IsNotExist(err) {
297 // startPage = page.InitPage
299 // startPage = page.InitPage
298 // }
300 // }
@@ -305,6 +307,7 @@ func New(app *app.App) tea.Model {
305 dialog: core.NewDialogCmp(),
307 dialog: core.NewDialogCmp(),
306 app: app,
308 app: app,
307 pages: map[page.PageID]tea.Model{
309 pages: map[page.PageID]tea.Model{
310+ page.ChatPage: page.NewChatPage(app),
308 page.LogsPage: page.NewLogsPage(),
311 page.LogsPage: page.NewLogsPage(),
309 page.InitPage: page.NewInitPage(),
312 page.InitPage: page.NewInitPage(),
310 page.ReplPage: page.NewReplPage(app),
313 page.ReplPage: page.NewReplPage(app),