on main help sign in
wip theme + new UI
aad4090Kujtim Hoxha
.opencode.json+2−2
@@ -1,11 +1,11 @@
1{
1{
2 "$schema": "./opencode-schema.json",
2 "$schema": "./opencode-schema.json",
3 "lsp": {
3 "lsp": {
4− "gopls": {
4+ "Go": {
5 "command": "gopls"
5 "command": "gopls"
6 }
6 }
7 },
7 },
8 "tui": {
8 "tui": {
9− "theme": "opencode-dark"
9+ "theme": "charm"
10 }
10 }
11}
11}
cspell.json+1−1
@@ -1 +1 @@
1−{"flagWords":[],"language":"en","words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable"],"version":"0.2"}
1+{"words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps"],"version":"0.2","language":"en","flagWords":[]}
diff.diffnew file+124
@@ -0,0 +1,124 @@
1+diff --git a/.opencode.json b/.opencode.json
2+index 75e357d..59be1e8 100644
3+--- a/.opencode.json
4++++ b/.opencode.json
5+@@ -6,6 +6,6 @@
6+ }
7+ },
8+ "tui": {
9+- "theme": "opencode-dark"
10++ "theme": "charm"
11+ }
12+ }
13+diff --git a/go.mod b/go.mod
14+index 18ad042..940a8e8 100644
15+--- a/go.mod
16++++ b/go.mod
17+@@ -36,6 +36,8 @@ require (
18+ github.com/stretchr/testify v1.10.0
19+ )
20+
21++require github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9 // indirect
22++
23+ require (
24+ cloud.google.com/go v0.116.0 // indirect
25+ cloud.google.com/go/auth v0.13.0 // indirect
26+diff --git a/go.sum b/go.sum
27+index f6e08b7..8f347ed 100644
28+--- a/go.sum
29++++ b/go.sum
30+@@ -84,6 +84,8 @@ github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa h1:JU05TLAB
31+ github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
32+ github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
33+ github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
34++github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9 h1:f6tG7ApqIvXTpgF6MZ+C4Ga7669eiW9BsMkXEjDFHfY=
35++github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9/go.mod h1:vr+xCFylsPYq2qSz+n5/jItjcK2/PgrKFMTI7VRR6CI=
36+ github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
37+ github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
38+ github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620 h1:/PN4jqP3ki9NvtHRrYZ9ewCutKZB6DK8frTW+Dj/MWs=
39+diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go
40+index 2ee0b04..52c4dae 100644
41+--- a/internal/tui/components/chat/chat.go
42++++ b/internal/tui/components/chat/chat.go
43+@@ -95,7 +95,7 @@ func lspsConfigured() string {
44+ func logoBlock() string {
45+ t := theme.CurrentTheme()
46+ return logo.Render(version.Version, true, logo.Opts{
47+- FieldColor: t.Accent(),
48++ FieldColor: t.Secondary(),
49+ TitleColorA: t.Primary(),
50+ TitleColorB: t.Secondary(),
51+ CharmColor: t.Primary(),
52+diff --git a/internal/tui/tui.go b/internal/tui/tui.go
53+index 9e8a62a..3f07956 100644
54+--- a/internal/tui/tui.go
55++++ b/internal/tui/tui.go
56+@@ -18,6 +18,7 @@ import (
57+ "github.com/opencode-ai/opencode/internal/tui/util"
58+ )
59+
60++// appModel represents the main application model that manages pages, dialogs, and UI state.
61+ type appModel struct {
62+ width, height int
63+ keyMap KeyMap
64+@@ -35,6 +36,7 @@ type appModel struct {
65+ completions completions.Completions
66+ }
67+
68++// Init initializes the application model and returns initial commands.
69+ func (a appModel) Init() tea.Cmd {
70+ var cmds []tea.Cmd
71+ cmd := a.pages[a.currentPage].Init()
72+@@ -46,6 +48,7 @@ func (a appModel) Init() tea.Cmd {
73+ return tea.Batch(cmds...)
74+ }
75+
76++// Update handles incoming messages and updates the application state.
77+ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
78+ var cmds []tea.Cmd
79+ var cmd tea.Cmd
80+@@ -111,6 +114,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
81+ return a, tea.Batch(cmds...)
82+ }
83+
84++// handleWindowResize processes window resize events and updates all components.
85+ func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
86+ var cmds []tea.Cmd
87+ msg.Height -= 1 // Make space for the status bar
88+@@ -134,6 +138,7 @@ func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
89+ return tea.Batch(cmds...)
90+ }
91+
92++// handleKeyPressMsg processes keyboard input and routes to appropriate handlers.
93+ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
94+ switch {
95+ // completions
96+@@ -182,11 +187,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
97+ }
98+ }
99+
100+-// RegisterCommand adds a command to the command dialog
101+-// func (a *appModel) RegisterCommand(cmd dialog.Command) {
102+-// a.commands = append(a.commands, cmd)
103+-// }
104+-
105++// moveToPage handles navigation between different pages in the application.
106+ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
107+ if a.app.CoderAgent.IsBusy() {
108+ // For now we don't move to any page if the agent is busy
109+@@ -209,6 +210,7 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
110+ return tea.Batch(cmds...)
111+ }
112+
113++// View renders the complete application interface including pages, dialogs, and overlays.
114+ func (a *appModel) View() tea.View {
115+ pageView := a.pages[a.currentPage].View()
116+ components := []string{
117+@@ -252,6 +254,7 @@ func (a *appModel) View() tea.View {
118+ return view
119+ }
120+
121++// New creates and initializes a new TUI application model.
122+ func New(app *app.App) tea.Model {
123+ startPage := page.ChatPage
124+ model := &appModel{
go.mod+1
@@ -18,6 +18,7 @@ require (
18 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
18 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
19 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
19 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
20 github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa
20 github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa
21+ github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9
21 github.com/fsnotify/fsnotify v1.8.0
22 github.com/fsnotify/fsnotify v1.8.0
22 github.com/go-logfmt/logfmt v0.6.0
23 github.com/go-logfmt/logfmt v0.6.0
23 github.com/google/uuid v1.6.0
24 github.com/google/uuid v1.6.0
go.sum+2
@@ -84,6 +84,8 @@ github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa h1:JU05TLAB
84github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
84github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
85github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
85github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
86github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
86github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
87+github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9 h1:f6tG7ApqIvXTpgF6MZ+C4Ga7669eiW9BsMkXEjDFHfY=
88+github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9/go.mod h1:vr+xCFylsPYq2qSz+n5/jItjcK2/PgrKFMTI7VRR6CI=
87github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
89github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
88github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
90github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
89github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620 h1:/PN4jqP3ki9NvtHRrYZ9ewCutKZB6DK8frTW+Dj/MWs=
91github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620 h1:/PN4jqP3ki9NvtHRrYZ9ewCutKZB6DK8frTW+Dj/MWs=
internal/tui/components/chat/chat.go+1−1
@@ -95,7 +95,7 @@ func lspsConfigured() string {
95func logoBlock() string {
95func logoBlock() string {
96 t := theme.CurrentTheme()
96 t := theme.CurrentTheme()
97 return logo.Render(version.Version, true, logo.Opts{
97 return logo.Render(version.Version, true, logo.Opts{
98− FieldColor: t.Accent(),
98+ FieldColor: t.Secondary(),
99 TitleColorA: t.Primary(),
99 TitleColorA: t.Primary(),
100 TitleColorB: t.Secondary(),
100 TitleColorB: t.Secondary(),
101 CharmColor: t.Primary(),
101 CharmColor: t.Primary(),
internal/tui/components/chat/editor/editor.go+23−41
@@ -261,29 +261,25 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
261}
261}
262
262
263func (m *editorCmp) View() tea.View {
263func (m *editorCmp) View() tea.View {
264− t := theme.CurrentTheme()
264+ t := styles.CurrentTheme()
265−
266− // Style the prompt with theme colors
267− style := lipgloss.NewStyle().
268− Padding(0, 0, 0, 1).
269− Bold(true).
270− Foreground(t.Primary())
271−
272 cursor := m.textarea.Cursor()
265 cursor := m.textarea.Cursor()
273− cursor.X = cursor.X + m.x + 2
266+ cursor.X = cursor.X + m.x + 1
274− cursor.Y = cursor.Y + m.y + 1
267+ cursor.Y = cursor.Y + m.y + 1 // adjust for padding
275 if len(m.attachments) == 0 {
268 if len(m.attachments) == 0 {
276− view := tea.NewView(lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View()))
269+ content := t.S().Base.Padding(1).Render(
270+ m.textarea.View(),
271+ )
272+ view := tea.NewView(content)
277 view.SetCursor(cursor)
273 view.SetCursor(cursor)
278 return view
274 return view
279 }
275 }
280− m.textarea.SetHeight(m.height - 1)
276+ content := t.S().Base.Padding(0, 1, 1, 1).Render(
281− view := tea.NewView(lipgloss.JoinVertical(lipgloss.Top,
277+ lipgloss.JoinVertical(lipgloss.Top,
282− m.attachmentsContent(),
278+ m.attachmentsContent(),
283− lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"),
284 m.textarea.View(),
279 m.textarea.View(),
285 ),
280 ),
286− ))
281+ )
282+ view := tea.NewView(content)
287 view.SetCursor(cursor)
283 view.SetCursor(cursor)
288 return view
284 return view
289}
285}
@@ -291,8 +287,8 @@ func (m *editorCmp) View() tea.View {
291func (m *editorCmp) SetSize(width, height int) tea.Cmd {
287func (m *editorCmp) SetSize(width, height int) tea.Cmd {
292 m.width = width
288 m.width = width
293 m.height = height
289 m.height = height
294− m.textarea.SetWidth(width - 3) // account for the prompt and padding right
290+ m.textarea.SetWidth(width - 2) // adjust for padding
295− m.textarea.SetHeight(height)
291+ m.textarea.SetHeight(height - 2) // adjust for padding
296 return nil
292 return nil
297}
293}
298
294
@@ -359,32 +355,18 @@ func (m *editorCmp) startCompletions() tea.Msg {
359}
355}
360
356
361func CreateTextArea(existing *textarea.Model) textarea.Model {
357func CreateTextArea(existing *textarea.Model) textarea.Model {
362− t := theme.CurrentTheme()
358+ t := styles.CurrentTheme()
363− bgColor := t.Background()
364− textColor := t.Text()
365− textMutedColor := t.TextMuted()
366−
367 ta := textarea.New()
359 ta := textarea.New()
368− s := textarea.DefaultDarkStyles()
369− b := s.Blurred
370− b.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
371− b.CursorLine = styles.BaseStyle().Background(bgColor)
372− b.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
373− b.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
374−
375− f := s.Focused
376− f.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
377− f.CursorLine = styles.BaseStyle().Background(bgColor)
378− f.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
379− f.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
380−
381− s.Focused = f
382− s.Blurred = b
383− ta.SetStyles(s)
360+ ta.SetStyles(t.S().TextArea)
384−
361+ ta.SetPromptFunc(2, func(lineIndex int) string {
385− ta.Prompt = " "
362+ if lineIndex == 0 {
363+ return "> "
364+ }
365+ return t.S().Muted.Render(": ")
366+ })
386 ta.ShowLineNumbers = false
367 ta.ShowLineNumbers = false
387 ta.CharLimit = -1
368 ta.CharLimit = -1
369+ ta.Placeholder = "Tell me more about this project..."
388 ta.SetVirtualCursor(false)
370 ta.SetVirtualCursor(false)
389
371
390 if existing != nil {
372 if existing != nil {
internal/tui/components/chat/sidebar/sidebar.gonew file+205
@@ -0,0 +1,205 @@
1+package sidebar
2+
3+import (
4+ "os"
5+ "strings"
6+
7+ tea "github.com/charmbracelet/bubbletea/v2"
8+ "github.com/charmbracelet/lipgloss/v2"
9+ "github.com/opencode-ai/opencode/internal/config"
10+ "github.com/opencode-ai/opencode/internal/pubsub"
11+ "github.com/opencode-ai/opencode/internal/session"
12+ "github.com/opencode-ai/opencode/internal/tui/components/chat"
13+ "github.com/opencode-ai/opencode/internal/tui/components/core"
14+ "github.com/opencode-ai/opencode/internal/tui/components/logo"
15+ "github.com/opencode-ai/opencode/internal/tui/layout"
16+ "github.com/opencode-ai/opencode/internal/tui/styles"
17+ "github.com/opencode-ai/opencode/internal/tui/util"
18+ "github.com/opencode-ai/opencode/internal/version"
19+)
20+
21+const (
22+ logoBreakpoint = 65
23+)
24+
25+type Sidebar interface {
26+ util.Model
27+ layout.Sizeable
28+}
29+
30+type sidebarCmp struct {
31+ width, height int
32+ session session.Session
33+ logo string
34+ cwd string
35+}
36+
37+func NewSidebarCmp() Sidebar {
38+ return &sidebarCmp{}
39+}
40+
41+func (m *sidebarCmp) Init() tea.Cmd {
42+ m.logo = m.logoBlock(false)
43+ m.cwd = cwd()
44+ return nil
45+}
46+
47+func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
48+ switch msg := msg.(type) {
49+ case chat.SessionSelectedMsg:
50+ if msg.ID != m.session.ID {
51+ m.session = msg
52+ }
53+ case pubsub.Event[session.Session]:
54+ if msg.Type == pubsub.UpdatedEvent {
55+ if m.session.ID == msg.Payload.ID {
56+ m.session = msg.Payload
57+ }
58+ }
59+ }
60+ return m, nil
61+}
62+
63+func (m *sidebarCmp) View() tea.View {
64+ t := styles.CurrentTheme()
65+ parts := []string{
66+ m.logo,
67+ }
68+
69+ if m.session.ID != "" {
70+ parts = append(parts, t.S().Muted.Render(m.session.Title), "")
71+ }
72+
73+ parts = append(parts,
74+ m.cwd,
75+ "",
76+ m.lspBlock(),
77+ "",
78+ m.mcpBlock(),
79+ )
80+
81+ return tea.NewView(
82+ lipgloss.JoinVertical(lipgloss.Left, parts...),
83+ )
84+}
85+
86+func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
87+ if width < logoBreakpoint && m.width >= logoBreakpoint {
88+ m.logo = m.logoBlock(true)
89+ } else if width >= logoBreakpoint && m.width < logoBreakpoint {
90+ m.logo = m.logoBlock(false)
91+ }
92+
93+ m.width = width
94+ m.height = height
95+ return nil
96+}
97+
98+func (m *sidebarCmp) GetSize() (int, int) {
99+ return m.width, m.height
100+}
101+
102+func (m *sidebarCmp) logoBlock(compact bool) string {
103+ t := styles.CurrentTheme()
104+ return logo.Render(version.Version, compact, logo.Opts{
105+ FieldColor: t.Primary,
106+ TitleColorA: t.Secondary,
107+ TitleColorB: t.Primary,
108+ CharmColor: t.Secondary,
109+ VersionColor: t.Primary,
110+ })
111+}
112+
113+func (m *sidebarCmp) lspBlock() string {
114+ maxWidth := min(m.width, 58)
115+ t := styles.CurrentTheme()
116+
117+ section := t.S().Muted.Render(
118+ core.Section("Configured LSPs", maxWidth),
119+ )
120+
121+ lspList := []string{section, ""}
122+
123+ lsp := config.Get().LSP
124+ if len(lsp) == 0 {
125+ return lipgloss.JoinVertical(
126+ lipgloss.Left,
127+ section,
128+ "",
129+ t.S().Muted.Render("No LSPs configured."),
130+ )
131+ }
132+
133+ for n, l := range lsp {
134+ iconColor := t.Success
135+ if l.Disabled {
136+ iconColor = t.FgMuted
137+ }
138+ lspList = append(lspList,
139+ core.Status(
140+ core.StatusOpts{
141+ IconColor: iconColor,
142+ Title: n,
143+ Description: l.Command,
144+ },
145+ m.width,
146+ ),
147+ )
148+ }
149+
150+ return lipgloss.JoinVertical(
151+ lipgloss.Left,
152+ lspList...,
153+ )
154+}
155+
156+func (m *sidebarCmp) mcpBlock() string {
157+ maxWidth := min(m.width, 58)
158+ t := styles.CurrentTheme()
159+
160+ section := t.S().Muted.Render(
161+ core.Section("Configured MCPs", maxWidth),
162+ )
163+
164+ mcpList := []string{section, ""}
165+
166+ mcp := config.Get().MCPServers
167+ if len(mcp) == 0 {
168+ return lipgloss.JoinVertical(
169+ lipgloss.Left,
170+ section,
171+ "",
172+ t.S().Muted.Render("No MCPs configured."),
173+ )
174+ }
175+
176+ for n, l := range mcp {
177+ iconColor := t.Success
178+ mcpList = append(mcpList,
179+ core.Status(
180+ core.StatusOpts{
181+ IconColor: iconColor,
182+ Title: n,
183+ Description: l.Command,
184+ },
185+ m.width,
186+ ),
187+ )
188+ }
189+
190+ return lipgloss.JoinVertical(
191+ lipgloss.Left,
192+ mcpList...,
193+ )
194+}
195+
196+func cwd() string {
197+ cwd := config.WorkingDirectory()
198+ t := styles.CurrentTheme()
199+ // replace home directory with ~
200+ homeDir, err := os.UserHomeDir()
201+ if err == nil {
202+ cwd = strings.ReplaceAll(cwd, homeDir, "~")
203+ }
204+ return t.S().Muted.Render(cwd)
205+}
internal/tui/components/core/helpers.gonew file+63
@@ -0,0 +1,63 @@
1+package core
2+
3+import (
4+ "image/color"
5+ "strings"
6+
7+ "github.com/charmbracelet/lipgloss/v2"
8+ "github.com/charmbracelet/x/ansi"
9+ "github.com/opencode-ai/opencode/internal/tui/styles"
10+)
11+
12+func Section(title string, width int) string {
13+ t := styles.CurrentTheme()
14+ char := "─"
15+ length := len(title) + 1
16+ remainingWidth := width - length
17+ if remainingWidth > 0 {
18+ title = title + " " + t.S().Subtle.Render(strings.Repeat(char, remainingWidth))
19+ }
20+ return title
21+}
22+
23+type StatusOpts struct {
24+ Icon string
25+ IconColor color.Color
26+ Title string
27+ TitleColor color.Color
28+ Description string
29+ DescriptionColor color.Color
30+}
31+
32+func Status(ops StatusOpts, width int) string {
33+ t := styles.CurrentTheme()
34+ icon := "●"
35+ iconColor := t.Success
36+ if ops.Icon != "" {
37+ icon = ops.Icon
38+ }
39+ if ops.IconColor != nil {
40+ iconColor = ops.IconColor
41+ }
42+ title := ops.Title
43+ titleColor := t.FgMuted
44+ if ops.TitleColor != nil {
45+ titleColor = ops.TitleColor
46+ }
47+ description := ops.Description
48+ descriptionColor := t.FgSubtle
49+ if ops.DescriptionColor != nil {
50+ descriptionColor = ops.DescriptionColor
51+ }
52+ icon = t.S().Base.Foreground(iconColor).Render(icon)
53+ title = t.S().Base.Foreground(titleColor).Render(title)
54+ if description != "" {
55+ description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2, "…")
56+ }
57+ description = t.S().Base.Foreground(descriptionColor).Render(description)
58+ return strings.Join([]string{
59+ icon,
60+ title,
61+ description,
62+ }, " ")
63+}
internal/tui/components/logo/logo.go+1−1
@@ -63,7 +63,7 @@ func Render(version string, compact bool, o Opts) string {
63 // Narrow version.
63 // Narrow version.
64 if compact {
64 if compact {
65 field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
65 field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
66− return strings.Join([]string{field, field, crush, field}, "\n")
66+ return strings.Join([]string{field, field, crush, field, ""}, "\n")
67 }
67 }
68
68
69 fieldHeight := lipgloss.Height(crush)
69 fieldHeight := lipgloss.Height(crush)
internal/tui/layout/container.go+4−5
@@ -4,7 +4,7 @@ import (
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/lipgloss/v2"
6 "github.com/charmbracelet/lipgloss/v2"
7− "github.com/opencode-ai/opencode/internal/tui/theme"
7+ "github.com/opencode-ai/opencode/internal/tui/styles"
8 "github.com/opencode-ai/opencode/internal/tui/util"
8 "github.com/opencode-ai/opencode/internal/tui/util"
9)
9)
10
10
@@ -46,12 +46,11 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
46}
46}
47
47
48func (c *container) View() tea.View {
48func (c *container) View() tea.View {
49− t := theme.CurrentTheme()
49+ t := styles.CurrentTheme()
50− style := lipgloss.NewStyle()
51 width := c.width
50 width := c.width
52 height := c.height
51 height := c.height
53
52
54− style = style.Background(t.Background())
53+ style := t.S().Base
55
54
56 // Apply border if any side is enabled
55 // Apply border if any side is enabled
57 if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
56 if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
@@ -69,7 +68,7 @@ func (c *container) View() tea.View {
69 width--
68 width--
70 }
69 }
71 style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
70 style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
72− style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
71+ style = style.BorderBackground(t.BgBase).BorderForeground(t.Border)
73 }
72 }
74 style = style.
73 style = style.
75 Width(width).
74 Width(width).
internal/tui/layout/split.go+42−8
@@ -22,14 +22,18 @@ type SplitPaneLayout interface {
22}
22}
23
23
24type splitPaneLayout struct {
24type splitPaneLayout struct {
25− width int
25+ width int
26− height int
26+ height int
27+
27 ratio float64
28 ratio float64
28 verticalRatio float64
29 verticalRatio float64
29
30
30 rightPanel Container
31 rightPanel Container
31 leftPanel Container
32 leftPanel Container
32 bottomPanel Container
33 bottomPanel Container
34+
35+ fixedBottomHeight int // Fixed height for the bottom panel, if any
36+ fixedRightWidth int // Fixed width for the right panel, if any
33}
37}
34
38
35type SplitPaneOption func(*splitPaneLayout)
39type SplitPaneOption func(*splitPaneLayout)
@@ -141,8 +145,17 @@ func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
141 var topHeight, bottomHeight int
145 var topHeight, bottomHeight int
142 var cmds []tea.Cmd
146 var cmds []tea.Cmd
143 if s.bottomPanel != nil {
147 if s.bottomPanel != nil {
148+ if s.fixedBottomHeight > 0 {
149+ bottomHeight = s.fixedBottomHeight
150+ topHeight = height - bottomHeight
151+ } else {
144− topHeight = int(float64(height) * s.verticalRatio)
152+ topHeight = int(float64(height) * s.verticalRatio)
145− bottomHeight = height - topHeight
153+ bottomHeight = height - topHeight
154+ if bottomHeight <= 0 {
155+ bottomHeight = 2
156+ topHeight = height - bottomHeight
157+ }
158+ }
146 } else {
159 } else {
147 topHeight = height
160 topHeight = height
148 bottomHeight = 0
161 bottomHeight = 0
@@ -150,8 +163,17 @@ func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
150
163
151 var leftWidth, rightWidth int
164 var leftWidth, rightWidth int
152 if s.leftPanel != nil && s.rightPanel != nil {
165 if s.leftPanel != nil && s.rightPanel != nil {
166+ if s.fixedRightWidth > 0 {
167+ rightWidth = s.fixedRightWidth
168+ leftWidth = width - rightWidth
169+ } else {
153− leftWidth = int(float64(width) * s.ratio)
170+ leftWidth = int(float64(width) * s.ratio)
154− rightWidth = width - leftWidth
171+ rightWidth = width - leftWidth
172+ if rightWidth <= 0 {
173+ rightWidth = 2
174+ leftWidth = width - rightWidth
175+ }
176+ }
155 } else if s.leftPanel != nil {
177 } else if s.leftPanel != nil {
156 leftWidth = width
178 leftWidth = width
157 rightWidth = 0
179 rightWidth = 0
@@ -260,8 +282,8 @@ func (s *splitPaneLayout) BindingKeys() []key.Binding {
260
282
261func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
283func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
262 layout := &splitPaneLayout{
284 layout := &splitPaneLayout{
263− ratio: 0.7,
285+ ratio: 0.8,
264− verticalRatio: 0.9, // Default 90% for top section, 10% for bottom
286+ verticalRatio: 0.92, // Default 90% for top section, 10% for bottom
265 }
287 }
266 for _, option := range options {
288 for _, option := range options {
267 option(layout)
289 option(layout)
@@ -298,3 +320,15 @@ func WithVerticalRatio(ratio float64) SplitPaneOption {
298 s.verticalRatio = ratio
320 s.verticalRatio = ratio
299 }
321 }
300}
322}
323+
324+func WithFixedBottomHeight(height int) SplitPaneOption {
325+ return func(s *splitPaneLayout) {
326+ s.fixedBottomHeight = height
327+ }
328+}
329+
330+func WithFixedRightWidth(width int) SplitPaneOption {
331+ return func(s *splitPaneLayout) {
332+ s.fixedRightWidth = width
333+ }
334+}
internal/tui/page/chat/chat.gonew file+173
@@ -0,0 +1,173 @@
1+package chat
2+
3+import (
4+ "context"
5+
6+ "github.com/charmbracelet/bubbles/v2/key"
7+ tea "github.com/charmbracelet/bubbletea/v2"
8+ "github.com/opencode-ai/opencode/internal/app"
9+ "github.com/opencode-ai/opencode/internal/message"
10+ "github.com/opencode-ai/opencode/internal/session"
11+ "github.com/opencode-ai/opencode/internal/tui/components/chat"
12+ "github.com/opencode-ai/opencode/internal/tui/components/chat/editor"
13+ "github.com/opencode-ai/opencode/internal/tui/components/chat/sidebar"
14+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
15+ "github.com/opencode-ai/opencode/internal/tui/layout"
16+ "github.com/opencode-ai/opencode/internal/tui/page"
17+ "github.com/opencode-ai/opencode/internal/tui/util"
18+)
19+
20+var ChatPage page.PageID = "chat"
21+
22+type chatPage struct {
23+ app *app.App
24+
25+ layout layout.SplitPaneLayout
26+
27+ session session.Session
28+}
29+
30+type ChatKeyMap struct {
31+ NewSession key.Binding
32+ Cancel key.Binding
33+}
34+
35+var keyMap = ChatKeyMap{
36+ NewSession: key.NewBinding(
37+ key.WithKeys("ctrl+n"),
38+ key.WithHelp("ctrl+n", "new session"),
39+ ),
40+ Cancel: key.NewBinding(
41+ key.WithKeys("esc"),
42+ key.WithHelp("esc", "cancel"),
43+ ),
44+}
45+
46+func (p *chatPage) Init() tea.Cmd {
47+ return p.layout.Init()
48+}
49+
50+func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
51+ var cmds []tea.Cmd
52+ switch msg := msg.(type) {
53+ case tea.WindowSizeMsg:
54+ cmd := p.layout.SetSize(msg.Width, msg.Height)
55+ cmds = append(cmds, cmd)
56+ case chat.SendMsg:
57+ cmd := p.sendMessage(msg.Text, msg.Attachments)
58+ if cmd != nil {
59+ return p, cmd
60+ }
61+ case commands.CommandRunCustomMsg:
62+ // Check if the agent is busy before executing custom commands
63+ if p.app.CoderAgent.IsBusy() {
64+ return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
65+ }
66+
67+ // Handle custom command execution
68+ cmd := p.sendMessage(msg.Content, nil)
69+ if cmd != nil {
70+ return p, cmd
71+ }
72+ case chat.SessionSelectedMsg:
73+ if p.session.ID == "" {
74+ cmd := p.setMessages()
75+ if cmd != nil {
76+ cmds = append(cmds, cmd)
77+ }
78+ }
79+ p.session = msg
80+ case tea.KeyPressMsg:
81+ switch {
82+ case key.Matches(msg, keyMap.NewSession):
83+ p.session = session.Session{}
84+ return p, tea.Batch(
85+ p.clearMessages(),
86+ util.CmdHandler(chat.SessionClearedMsg{}),
87+ )
88+ case key.Matches(msg, keyMap.Cancel):
89+ if p.session.ID != "" {
90+ // Cancel the current session's generation process
91+ // This allows users to interrupt long-running operations
92+ p.app.CoderAgent.Cancel(p.session.ID)
93+ return p, nil
94+ }
95+ }
96+ }
97+ u, cmd := p.layout.Update(msg)
98+ cmds = append(cmds, cmd)
99+ p.layout = u.(layout.SplitPaneLayout)
100+
101+ return p, tea.Batch(cmds...)
102+}
103+
104+func (p *chatPage) setMessages() tea.Cmd {
105+ messagesContainer := layout.NewContainer(
106+ chat.NewMessagesListCmp(p.app),
107+ layout.WithPadding(1, 1, 0, 1),
108+ )
109+ return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
110+}
111+
112+func (p *chatPage) clearMessages() tea.Cmd {
113+ return p.layout.ClearLeftPanel()
114+}
115+
116+func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
117+ var cmds []tea.Cmd
118+ if p.session.ID == "" {
119+ session, err := p.app.Sessions.Create(context.Background(), "New Session")
120+ if err != nil {
121+ return util.ReportError(err)
122+ }
123+
124+ p.session = session
125+ cmd := p.setMessages()
126+ if cmd != nil {
127+ cmds = append(cmds, cmd)
128+ }
129+ cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
130+ }
131+
132+ _, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
133+ if err != nil {
134+ return util.ReportError(err)
135+ }
136+ return tea.Batch(cmds...)
137+}
138+
139+func (p *chatPage) SetSize(width, height int) tea.Cmd {
140+ return p.layout.SetSize(width, height)
141+}
142+
143+func (p *chatPage) GetSize() (int, int) {
144+ return p.layout.GetSize()
145+}
146+
147+func (p *chatPage) View() tea.View {
148+ return p.layout.View()
149+}
150+
151+func (p *chatPage) BindingKeys() []key.Binding {
152+ bindings := layout.KeyMapToSlice(keyMap)
153+ return bindings
154+}
155+
156+func NewChatPage(app *app.App) util.Model {
157+ sidebarContainer := layout.NewContainer(
158+ sidebar.NewSidebarCmp(),
159+ layout.WithPadding(1, 1, 1, 1),
160+ )
161+ editorContainer := layout.NewContainer(
162+ editor.NewEditorCmp(app),
163+ )
164+ return &chatPage{
165+ app: app,
166+ layout: layout.NewSplitPane(
167+ layout.WithRightPanel(sidebarContainer),
168+ layout.WithBottomPanel(editorContainer),
169+ layout.WithFixedBottomHeight(3),
170+ layout.WithFixedRightWidth(31),
171+ ),
172+ }
173+}
internal/tui/page/chat/keys.gonew file+1
@@ -0,0 +1 @@
1+package chat
internal/tui/styles/crush.gonew file+44
@@ -0,0 +1,44 @@
1+package styles
2+
3+import (
4+ "github.com/charmbracelet/lipgloss/v2"
5+ "github.com/charmbracelet/x/exp/charmtone"
6+)
7+
8+func NewCrushTheme() *Theme {
9+ return &Theme{
10+ Name: "crush",
11+ IsDark: true,
12+
13+ Primary: lipgloss.Color(charmtone.Charple.Hex()),
14+ Secondary: lipgloss.Color(charmtone.Dolly.Hex()),
15+ Tertiary: lipgloss.Color(charmtone.Bok.Hex()),
16+ Accent: lipgloss.Color(charmtone.Zest.Hex()),
17+
18+ // Backgrounds
19+ BgBase: lipgloss.Color(charmtone.Pepper.Hex()),
20+ BgSubtle: lipgloss.Color(charmtone.Charcoal.Hex()),
21+ BgOverlay: lipgloss.Color(charmtone.Iron.Hex()),
22+
23+ // Foregrounds
24+ FgBase: lipgloss.Color(charmtone.Ash.Hex()),
25+ FgMuted: lipgloss.Color(charmtone.Squid.Hex()),
26+ FgSubtle: lipgloss.Color(charmtone.Oyster.Hex()),
27+
28+ // Borders
29+ Border: lipgloss.Color(charmtone.Charcoal.Hex()),
30+ BorderFocus: lipgloss.Color(charmtone.Charple.Hex()),
31+
32+ // Status
33+ Success: lipgloss.Color(charmtone.Guac.Hex()),
34+ Error: lipgloss.Color(charmtone.Sriracha.Hex()),
35+ Warning: lipgloss.Color(charmtone.Uni.Hex()),
36+ Info: lipgloss.Color(charmtone.Malibu.Hex()),
37+
38+ // TODO: fix this.
39+ SyntaxBg: lipgloss.Color("#1C1C1F"),
40+ SyntaxKeyword: lipgloss.Color("#FF6DFE"),
41+ SyntaxString: lipgloss.Color("#E8FE96"),
42+ SyntaxComment: lipgloss.Color("#6B6F85"),
43+ }
44+}
internal/tui/styles/theme.gonew file+229
@@ -0,0 +1,229 @@
1+package styles
2+
3+import (
4+ "fmt"
5+ "image/color"
6+
7+ "github.com/charmbracelet/bubbles/v2/textarea"
8+ tea "github.com/charmbracelet/bubbletea/v2"
9+ "github.com/charmbracelet/lipgloss/v2"
10+)
11+
12+type Theme struct {
13+ Name string
14+ IsDark bool
15+
16+ Primary color.Color
17+ Secondary color.Color
18+ Tertiary color.Color
19+ Accent color.Color
20+
21+ BgBase color.Color
22+ BgSubtle color.Color
23+ BgOverlay color.Color
24+
25+ FgBase color.Color
26+ FgMuted color.Color
27+ FgSubtle color.Color
28+
29+ Border color.Color
30+ BorderFocus color.Color
31+
32+ Success color.Color
33+ Error color.Color
34+ Warning color.Color
35+ Info color.Color
36+
37+ // TODO: add more syntax colors, maybe just use a chroma theme here.
38+ SyntaxBg color.Color
39+ SyntaxKeyword color.Color
40+ SyntaxString color.Color
41+ SyntaxComment color.Color
42+
43+ styles *Styles
44+}
45+
46+type Styles struct {
47+ Base lipgloss.Style
48+
49+ Title lipgloss.Style
50+ Subtitle lipgloss.Style
51+ Text lipgloss.Style
52+ Muted lipgloss.Style
53+ Subtle lipgloss.Style
54+
55+ Success lipgloss.Style
56+ Error lipgloss.Style
57+ Warning lipgloss.Style
58+ Info lipgloss.Style
59+
60+ // Inputs
61+ TextArea textarea.Styles
62+}
63+
64+func (t *Theme) S() *Styles {
65+ if t.styles == nil {
66+ t.styles = t.buildStyles()
67+ }
68+ return t.styles
69+}
70+
71+func (t *Theme) buildStyles() *Styles {
72+ base := lipgloss.NewStyle().
73+ Background(t.BgBase).
74+ Foreground(t.FgBase)
75+ return &Styles{
76+ Base: base,
77+
78+ Title: base.
79+ Foreground(t.Accent).
80+ Bold(true),
81+
82+ Subtitle: base.
83+ Foreground(t.Secondary).
84+ Bold(true),
85+
86+ Text: base,
87+
88+ Muted: base.Foreground(t.FgMuted),
89+
90+ Subtle: base.Foreground(t.FgSubtle),
91+
92+ Success: base.Foreground(t.Success),
93+
94+ Error: base.Foreground(t.Error),
95+
96+ Warning: base.Foreground(t.Warning),
97+
98+ Info: base.Foreground(t.Info),
99+
100+ TextArea: textarea.Styles{
101+ Focused: textarea.StyleState{
102+ Base: base,
103+ Text: base,
104+ LineNumber: base.Foreground(t.FgSubtle),
105+ CursorLine: base,
106+ CursorLineNumber: base.Foreground(t.FgSubtle),
107+ Placeholder: base.Foreground(t.FgMuted),
108+ Prompt: base.Foreground(t.Tertiary),
109+ },
110+ Blurred: textarea.StyleState{
111+ Base: base,
112+ Text: base.Foreground(t.FgMuted),
113+ LineNumber: base.Foreground(t.FgMuted),
114+ CursorLine: base,
115+ CursorLineNumber: base.Foreground(t.FgMuted),
116+ Placeholder: base.Foreground(t.FgMuted),
117+ Prompt: base.Foreground(t.FgMuted),
118+ },
119+ Cursor: textarea.CursorStyle{
120+ Color: t.Secondary,
121+ Shape: tea.CursorBar,
122+ Blink: true,
123+ },
124+ },
125+ }
126+}
127+
128+type Manager struct {
129+ themes map[string]*Theme
130+ current *Theme
131+}
132+
133+var defaultManager *Manager
134+
135+func SetDefaultManager(m *Manager) {
136+ defaultManager = m
137+}
138+
139+func DefaultManager() *Manager {
140+ if defaultManager == nil {
141+ defaultManager = NewManager("crush")
142+ }
143+ return defaultManager
144+}
145+
146+func CurrentTheme() *Theme {
147+ if defaultManager == nil {
148+ defaultManager = NewManager("crush")
149+ }
150+ return defaultManager.Current()
151+}
152+
153+func NewManager(defaultTheme string) *Manager {
154+ m := &Manager{
155+ themes: make(map[string]*Theme),
156+ }
157+
158+ m.Register(NewCrushTheme())
159+
160+ m.current = m.themes[defaultTheme]
161+
162+ return m
163+}
164+
165+func (m *Manager) Register(theme *Theme) {
166+ m.themes[theme.Name] = theme
167+}
168+
169+func (m *Manager) Current() *Theme {
170+ return m.current
171+}
172+
173+func (m *Manager) SetTheme(name string) error {
174+ if theme, ok := m.themes[name]; ok {
175+ m.current = theme
176+ return nil
177+ }
178+ return fmt.Errorf("theme %s not found", name)
179+}
180+
181+func (m *Manager) List() []string {
182+ names := make([]string, 0, len(m.themes))
183+ for name := range m.themes {
184+ names = append(names, name)
185+ }
186+ return names
187+}
188+
189+// ParseHex converts hex string to color
190+func ParseHex(hex string) color.Color {
191+ var r, g, b uint8
192+ fmt.Sscanf(hex, "#%02x%02x%02x", &r, &g, &b)
193+ return color.RGBA{R: r, G: g, B: b, A: 255}
194+}
195+
196+// Alpha returns a color with transparency
197+func Alpha(c color.Color, alpha uint8) color.Color {
198+ r, g, b, _ := c.RGBA()
199+ return color.RGBA{
200+ R: uint8(r >> 8),
201+ G: uint8(g >> 8),
202+ B: uint8(b >> 8),
203+ A: alpha,
204+ }
205+}
206+
207+// Darken makes a color darker by percentage (0-100)
208+func Darken(c color.Color, percent float64) color.Color {
209+ r, g, b, a := c.RGBA()
210+ factor := 1.0 - percent/100.0
211+ return color.RGBA{
212+ R: uint8(float64(r>>8) * factor),
213+ G: uint8(float64(g>>8) * factor),
214+ B: uint8(float64(b>>8) * factor),
215+ A: uint8(a >> 8),
216+ }
217+}
218+
219+// Lighten makes a color lighter by percentage (0-100)
220+func Lighten(c color.Color, percent float64) color.Color {
221+ r, g, b, a := c.RGBA()
222+ factor := percent / 100.0
223+ return color.RGBA{
224+ R: uint8(min(255, float64(r>>8)+255*factor)),
225+ G: uint8(min(255, float64(g>>8)+255*factor)),
226+ B: uint8(min(255, float64(b>>8)+255*factor)),
227+ A: uint8(a >> 8),
228+ }
229+}
internal/tui/tui.go+14−9
@@ -14,10 +14,12 @@ import (
14 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
14 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
15 "github.com/opencode-ai/opencode/internal/tui/layout"
15 "github.com/opencode-ai/opencode/internal/tui/layout"
16 "github.com/opencode-ai/opencode/internal/tui/page"
16 "github.com/opencode-ai/opencode/internal/tui/page"
17− "github.com/opencode-ai/opencode/internal/tui/theme"
17+ "github.com/opencode-ai/opencode/internal/tui/page/chat"
18+ "github.com/opencode-ai/opencode/internal/tui/styles"
18 "github.com/opencode-ai/opencode/internal/tui/util"
19 "github.com/opencode-ai/opencode/internal/tui/util"
19)
20)
20
21
22+// appModel represents the main application model that manages pages, dialogs, and UI state.
21type appModel struct {
23type appModel struct {
22 width, height int
24 width, height int
23 keyMap KeyMap
25 keyMap KeyMap
@@ -35,6 +37,7 @@ type appModel struct {
35 completions completions.Completions
37 completions completions.Completions
36}
38}
37
39
40+// Init initializes the application model and returns initial commands.
38func (a appModel) Init() tea.Cmd {
41func (a appModel) Init() tea.Cmd {
39 var cmds []tea.Cmd
42 var cmds []tea.Cmd
40 cmd := a.pages[a.currentPage].Init()
43 cmd := a.pages[a.currentPage].Init()
@@ -46,6 +49,7 @@ func (a appModel) Init() tea.Cmd {
46 return tea.Batch(cmds...)
49 return tea.Batch(cmds...)
47}
50}
48
51
52+// Update handles incoming messages and updates the application state.
49func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
53func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
50 var cmds []tea.Cmd
54 var cmds []tea.Cmd
51 var cmd tea.Cmd
55 var cmd tea.Cmd
@@ -111,6 +115,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
111 return a, tea.Batch(cmds...)
115 return a, tea.Batch(cmds...)
112}
116}
113
117
118+// handleWindowResize processes window resize events and updates all components.
114func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
119func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
115 var cmds []tea.Cmd
120 var cmds []tea.Cmd
116 msg.Height -= 1 // Make space for the status bar
121 msg.Height -= 1 // Make space for the status bar
@@ -134,6 +139,7 @@ func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
134 return tea.Batch(cmds...)
139 return tea.Batch(cmds...)
135}
140}
136
141
142+// handleKeyPressMsg processes keyboard input and routes to appropriate handlers.
137func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
143func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
138 switch {
144 switch {
139 // completions
145 // completions
@@ -182,11 +188,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
182 }
188 }
183}
189}
184
190
185−// RegisterCommand adds a command to the command dialog
191+// moveToPage handles navigation between different pages in the application.
186−// func (a *appModel) RegisterCommand(cmd dialog.Command) {
187−// a.commands = append(a.commands, cmd)
188−// }
189−
190func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
192func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
191 if a.app.CoderAgent.IsBusy() {
193 if a.app.CoderAgent.IsBusy() {
192 // For now we don't move to any page if the agent is busy
194 // For now we don't move to any page if the agent is busy
@@ -209,6 +211,7 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
209 return tea.Batch(cmds...)
211 return tea.Batch(cmds...)
210}
212}
211
213
214+// View renders the complete application interface including pages, dialogs, and overlays.
212func (a *appModel) View() tea.View {
215func (a *appModel) View() tea.View {
213 pageView := a.pages[a.currentPage].View()
216 pageView := a.pages[a.currentPage].View()
214 components := []string{
217 components := []string{
@@ -220,7 +223,6 @@ func (a *appModel) View() tea.View {
220 layers := []*lipgloss.Layer{
223 layers := []*lipgloss.Layer{
221 lipgloss.NewLayer(appView),
224 lipgloss.NewLayer(appView),
222 }
225 }
223− t := theme.CurrentTheme()
224 if a.dialog.HasDialogs() {
226 if a.dialog.HasDialogs() {
225 layers = append(
227 layers = append(
226 layers,
228 layers,
@@ -246,12 +248,15 @@ func (a *appModel) View() tea.View {
246 canvas := lipgloss.NewCanvas(
248 canvas := lipgloss.NewCanvas(
247 layers...,
249 layers...,
248 )
250 )
251+
252+ t := styles.CurrentTheme()
249 view := tea.NewView(canvas.Render())
253 view := tea.NewView(canvas.Render())
250− view.SetBackgroundColor(t.Background())
254+ view.SetBackgroundColor(t.BgBase)
251 view.SetCursor(cursor)
255 view.SetCursor(cursor)
252 return view
256 return view
253}
257}
254
258
259+// New creates and initializes a new TUI application model.
255func New(app *app.App) tea.Model {
260func New(app *app.App) tea.Model {
256 startPage := page.ChatPage
261 startPage := page.ChatPage
257 model := &appModel{
262 model := &appModel{
@@ -262,7 +267,7 @@ func New(app *app.App) tea.Model {
262 keyMap: DefaultKeyMap(),
267 keyMap: DefaultKeyMap(),
263
268
264 pages: map[page.PageID]util.Model{
269 pages: map[page.PageID]util.Model{
265− page.ChatPage: page.NewChatPage(app),
270+ page.ChatPage: chat.NewChatPage(app),
266 page.LogsPage: page.NewLogsPage(),
271 page.LogsPage: page.NewLogsPage(),
267 },
272 },
268
273
todos.mdnew file+8
@@ -0,0 +1,8 @@
1+# Chat Page
2+
3+## Landing page
4+
5+- [ ] Implement the logo landing page
6+- [ ] Add cwd improved
7+- [ ] Implement Active LSPs
8+- [ ] Implement Active MCPs