Detailed changes
@@ -1,124 +0,0 @@
-diff --git a/.opencode.json b/.opencode.json
-index 75e357d..59be1e8 100644
---- a/.opencode.json
-+++ b/.opencode.json
-@@ -6,6 +6,6 @@
- }
- },
- "tui": {
-- "theme": "opencode-dark"
-+ "theme": "charm"
- }
- }
-diff --git a/go.mod b/go.mod
-index 18ad042..940a8e8 100644
---- a/go.mod
-+++ b/go.mod
-@@ -36,6 +36,8 @@ require (
- github.com/stretchr/testify v1.10.0
- )
-
-+require github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9 // indirect
-+
- require (
- cloud.google.com/go v0.116.0 // indirect
- cloud.google.com/go/auth v0.13.0 // indirect
-diff --git a/go.sum b/go.sum
-index f6e08b7..8f347ed 100644
---- a/go.sum
-+++ b/go.sum
-@@ -84,6 +84,8 @@ github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa h1:JU05TLAB
- github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
- github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
- github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
-+github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9 h1:f6tG7ApqIvXTpgF6MZ+C4Ga7669eiW9BsMkXEjDFHfY=
-+github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9/go.mod h1:vr+xCFylsPYq2qSz+n5/jItjcK2/PgrKFMTI7VRR6CI=
- github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
- github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
- github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620 h1:/PN4jqP3ki9NvtHRrYZ9ewCutKZB6DK8frTW+Dj/MWs=
-diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go
-index 2ee0b04..52c4dae 100644
---- a/internal/tui/components/chat/chat.go
-+++ b/internal/tui/components/chat/chat.go
-@@ -95,7 +95,7 @@ func lspsConfigured() string {
- func logoBlock() string {
- t := theme.CurrentTheme()
- return logo.Render(version.Version, true, logo.Opts{
-- FieldColor: t.Accent(),
-+ FieldColor: t.Secondary(),
- TitleColorA: t.Primary(),
- TitleColorB: t.Secondary(),
- CharmColor: t.Primary(),
-diff --git a/internal/tui/tui.go b/internal/tui/tui.go
-index 9e8a62a..3f07956 100644
---- a/internal/tui/tui.go
-+++ b/internal/tui/tui.go
-@@ -18,6 +18,7 @@ import (
- "github.com/opencode-ai/opencode/internal/tui/util"
- )
-
-+// appModel represents the main application model that manages pages, dialogs, and UI state.
- type appModel struct {
- width, height int
- keyMap KeyMap
-@@ -35,6 +36,7 @@ type appModel struct {
- completions completions.Completions
- }
-
-+// Init initializes the application model and returns initial commands.
- func (a appModel) Init() tea.Cmd {
- var cmds []tea.Cmd
- cmd := a.pages[a.currentPage].Init()
-@@ -46,6 +48,7 @@ func (a appModel) Init() tea.Cmd {
- return tea.Batch(cmds...)
- }
-
-+// Update handles incoming messages and updates the application state.
- func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- var cmd tea.Cmd
-@@ -111,6 +114,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- return a, tea.Batch(cmds...)
- }
-
-+// handleWindowResize processes window resize events and updates all components.
- func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
- var cmds []tea.Cmd
- msg.Height -= 1 // Make space for the status bar
-@@ -134,6 +138,7 @@ func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
- return tea.Batch(cmds...)
- }
-
-+// handleKeyPressMsg processes keyboard input and routes to appropriate handlers.
- func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
- switch {
- // completions
-@@ -182,11 +187,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
- }
- }
-
--// RegisterCommand adds a command to the command dialog
--// func (a *appModel) RegisterCommand(cmd dialog.Command) {
--// a.commands = append(a.commands, cmd)
--// }
--
-+// moveToPage handles navigation between different pages in the application.
- func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
- if a.app.CoderAgent.IsBusy() {
- // For now we don't move to any page if the agent is busy
-@@ -209,6 +210,7 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
- return tea.Batch(cmds...)
- }
-
-+// View renders the complete application interface including pages, dialogs, and overlays.
- func (a *appModel) View() tea.View {
- pageView := a.pages[a.currentPage].View()
- components := []string{
-@@ -252,6 +254,7 @@ func (a *appModel) View() tea.View {
- return view
- }
-
-+// New creates and initializes a new TUI application model.
- func New(app *app.App) tea.Model {
- startPage := page.ChatPage
- model := &appModel{
@@ -11,9 +11,8 @@ require (
github.com/anthropics/anthropic-sdk-go v1.4.0
github.com/aymanbagabas/go-udiff v0.2.0
github.com/bmatcuk/doublestar/v4 v4.8.1
- github.com/catppuccin/go v0.3.0
github.com/charlievieth/fastwalk v1.0.11
- github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f
+ github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
@@ -23,9 +22,6 @@ require (
github.com/go-logfmt/logfmt v0.6.0
github.com/google/uuid v1.6.0
github.com/mark3labs/mcp-go v0.17.0
- github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
- github.com/muesli/reflow v0.3.0
- github.com/muesli/termenv v0.16.0
github.com/ncruces/go-sqlite3 v0.25.0
github.com/openai/openai-go v0.1.0-beta.2
github.com/pressly/goose/v3 v3.24.2
@@ -37,6 +33,8 @@ require (
github.com/stretchr/testify v1.10.0
)
+require github.com/dustin/go-humanize v1.0.1 // indirect
+
require (
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.13.0 // indirect
@@ -60,7 +58,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
- github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
@@ -85,7 +82,6 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0
- github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
@@ -58,24 +58,18 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
-github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
-github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
-github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318 h1:f8Q0ybZGxT+St1JfPM7yoz/XFpbmtodcIehaom/9XT8=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603122936-f1a3fad2b64e h1:+3I/1v7vbN0Vf8Tjm3Q0zdLQqjOM/TjQBvoRDQtoAss=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603122936-f1a3fad2b64e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f h1:vvNB+i59Wp3L6gYcpuhfAdNjr4/e6qM/st3ySWfmZnU=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607105053-36addcd3ab8c h1:Dgy7cOR3skvJjGVnhyaixW6ugxVxLtSjRCiMRSdbXSc=
+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607105053-36addcd3ab8c/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174 h1:TlVW+df0rdU/osP0O8DIVS9WFOAzXe3nuiMwJR4n+CA=
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
@@ -162,21 +156,14 @@ github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi9
github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
-github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
-github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
-github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-sqlite3 v0.25.0 h1:trugKUs98Zwy9KwRr/EUxZHL92LYt7UqcKqAfpGpK+I=
github.com/ncruces/go-sqlite3 v0.25.0/go.mod h1:n6Z7036yFilJx04yV0mi5JWaF66rUmXn1It9Ux8dx68=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -196,7 +183,6 @@ github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJz
github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
-github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -312,7 +298,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -0,0 +1,141 @@
+package filepicker
+
+import (
+ "os"
+ "strings"
+
+ "github.com/charmbracelet/bubbles/v2/filepicker"
+ tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/charmbracelet/lipgloss/v2"
+ "github.com/opencode-ai/opencode/internal/tui/components/core"
+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
+ "github.com/opencode-ai/opencode/internal/tui/styles"
+)
+
+const (
+ maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
+ FilePickerID = "filepicker"
+ fileSelectionHight = 10
+)
+
+type FilePicker interface {
+ dialogs.DialogModel
+}
+
+type filePicker struct {
+ wWidth int
+ wHeight int
+ width int
+ filepicker filepicker.Model
+ selectedFile string
+}
+
+func NewFilePickerCmp() FilePicker {
+ t := styles.CurrentTheme()
+ fp := filepicker.New()
+ fp.AllowedTypes = []string{".jpg", ".jpeg", ".png"}
+ fp.CurrentDirectory, _ = os.UserHomeDir()
+ fp.ShowPermissions = false
+ fp.ShowSize = false
+ fp.AutoHeight = false
+ fp.Styles = t.S().FilePicker
+ fp.Cursor = ""
+ fp.SetHeight(fileSelectionHight)
+
+ return &filePicker{filepicker: fp}
+}
+
+func (m *filePicker) Init() tea.Cmd {
+ return m.filepicker.Init()
+}
+
+func (m *filePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.wWidth = msg.Width
+ m.wHeight = msg.Height
+ m.width = min(70, m.wWidth)
+ styles := m.filepicker.Styles
+ styles.Directory = styles.Directory.Width(m.width - 4)
+ styles.Selected = styles.Selected.PaddingLeft(1).Width(m.width - 4)
+ styles.DisabledSelected = styles.DisabledSelected.PaddingLeft(1).Width(m.width - 4)
+ styles.File = styles.File.Width(m.width)
+ m.filepicker.Styles = styles
+ return m, nil
+ }
+
+ var cmd tea.Cmd
+ m.filepicker, cmd = m.filepicker.Update(msg)
+
+ // Did the user select a file?
+ if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
+ // Get the path of the selected file.
+ m.selectedFile = path
+ }
+
+ return m, cmd
+}
+
+func (m *filePicker) View() tea.View {
+ t := styles.CurrentTheme()
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Add Image", m.width-4)),
+ m.imagePreview(),
+ m.filepicker.View(),
+ )
+ return tea.NewView(m.style().Render(content))
+}
+
+func (m *filePicker) currentImage() string {
+ for _, ext := range m.filepicker.AllowedTypes {
+ if strings.HasSuffix(m.filepicker.HighlightedPath(), ext) {
+ return m.filepicker.HighlightedPath()
+ }
+ }
+ return ""
+}
+
+func (m *filePicker) imagePreview() string {
+ if m.currentImage() == "" {
+ return m.imagePreviewStyle().Render()
+ }
+
+ return ""
+}
+
+func (m *filePicker) imagePreviewStyle() lipgloss.Style {
+ t := styles.CurrentTheme()
+ w, h := m.imagePreviewSize()
+ return t.S().Base.
+ Width(w).
+ Height(h).
+ Margin(1).
+ Background(t.BgOverlay)
+}
+
+func (m *filePicker) imagePreviewSize() (int, int) {
+ return m.width - 4, min(20, m.wHeight/2)
+}
+
+func (m *filePicker) style() lipgloss.Style {
+ t := styles.CurrentTheme()
+ return t.S().Base.
+ Width(m.width).
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(t.BorderFocus)
+}
+
+// ID implements FilePicker.
+func (m *filePicker) ID() dialogs.DialogID {
+ return FilePickerID
+}
+
+// Position implements FilePicker.
+func (m *filePicker) Position() (int, int) {
+ row := m.wHeight/4 - 2 // just a bit above the center
+ col := m.wWidth / 2
+ col -= m.width / 2
+ return row, col
+}
@@ -0,0 +1,73 @@
+package filepicker
+
+import (
+ "github.com/charmbracelet/bubbles/v2/key"
+ "github.com/opencode-ai/opencode/internal/tui/layout"
+)
+
+// KeyMap defines keyboard bindings for dialog management.
+type KeyMap struct {
+ Select,
+ Down,
+ Up,
+ Forward,
+ Backward,
+ InsertCWD,
+ Close key.Binding
+}
+
+func DefaultKeyMap() KeyMap {
+ return KeyMap{
+ Select: key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "accept"),
+ ),
+ Down: key.NewBinding(
+ key.WithKeys("down", "j"),
+ key.WithHelp("down/j", "move down"),
+ ),
+ Up: key.NewBinding(
+ key.WithKeys("up", "k"),
+ key.WithHelp("up/k", "move up"),
+ ),
+ Forward: key.NewBinding(
+ key.WithKeys("right", "l"),
+ key.WithHelp("right/l", "move forward"),
+ ),
+ Backward: key.NewBinding(
+ key.WithKeys("left", "h"),
+ key.WithHelp("left/h", "move backward"),
+ ),
+ InsertCWD: key.NewBinding(
+ key.WithKeys("i"),
+ key.WithHelp("i", "manual path input"),
+ ),
+ Close: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "close/exit"),
+ ),
+ }
+}
+
+// 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.InsertCWD,
+ key.NewBinding(
+ key.WithHelp("↑↓←→", "navigate"),
+ ),
+ k.Select,
+ k.Close,
+ }
+}
@@ -6,12 +6,11 @@ import (
)
type KeyMap struct {
- Logs key.Binding
- Quit key.Binding
- Help key.Binding
- Commands key.Binding
- Sessions key.Binding
- FilePicker key.Binding
+ Logs key.Binding
+ Quit key.Binding
+ Help key.Binding
+ Commands key.Binding
+ Sessions key.Binding
}
func DefaultKeyMap() KeyMap {
@@ -37,10 +36,6 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("ctrl+s"),
key.WithHelp("ctrl+s", "sessions"),
),
- FilePicker: key.NewBinding(
- key.WithKeys("ctrl+f"),
- key.WithHelp("ctrl+f", "select files to upload"),
- ),
}
}
@@ -24,17 +24,20 @@ type ChatFocusedMsg struct {
Focused bool // True if the chat input is focused, false otherwise
}
-type chatPage struct {
- app *app.App
+type (
+ OpenFilePickerMsg struct{}
+ chatPage struct {
+ app *app.App
- layout layout.SplitPaneLayout
+ layout layout.SplitPaneLayout
- session session.Session
+ session session.Session
- keyMap KeyMap
+ keyMap KeyMap
- chatFocused bool
-}
+ chatFocused bool
+ }
+)
func (p *chatPage) Init() tea.Cmd {
cmd := p.layout.Init()
@@ -83,6 +86,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
util.CmdHandler(chat.SessionClearedMsg{}),
)
+ case key.Matches(msg, p.keyMap.FilePicker):
+ return p, util.CmdHandler(OpenFilePickerMsg{})
case key.Matches(msg, p.keyMap.Tab):
logging.Info("Tab key pressed, toggling chat focus")
if p.session.ID == "" {
@@ -7,6 +7,7 @@ import (
type KeyMap struct {
NewSession key.Binding
+ FilePicker key.Binding
Cancel key.Binding
Tab key.Binding
}
@@ -25,6 +26,10 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("tab"),
key.WithHelp("tab", "change focus"),
),
+ FilePicker: key.NewBinding(
+ key.WithKeys("ctrl+f"),
+ key.WithHelp("ctrl+f", "select files to upload"),
+ ),
}
}
@@ -5,6 +5,7 @@ import (
"image/color"
"strings"
+ "github.com/charmbracelet/bubbles/v2/filepicker"
"github.com/charmbracelet/bubbles/v2/help"
"github.com/charmbracelet/bubbles/v2/textarea"
"github.com/charmbracelet/bubbles/v2/textinput"
@@ -110,6 +111,9 @@ type Styles struct {
// Diff
Diff Diff
+
+ // FilePicker
+ FilePicker filepicker.Styles
}
func (t *Theme) S() *Styles {
@@ -430,6 +434,20 @@ func (t *Theme) buildStyles() *Styles {
AddedLineNumberBg: t.GreenDark,
RemovedLineNumberBg: t.RedDark,
},
+
+ FilePicker: filepicker.Styles{
+ DisabledCursor: base.Foreground(t.FgMuted),
+ Cursor: base.Foreground(t.FgBase),
+ Symlink: base.Foreground(t.FgSubtle),
+ Directory: base.Foreground(t.Primary),
+ File: base.Foreground(t.FgBase),
+ DisabledFile: base.Foreground(t.FgMuted),
+ DisabledSelected: base.Background(t.BgOverlay).Foreground(t.FgMuted),
+ Permission: base.Foreground(t.FgMuted),
+ Selected: base.Background(t.Primary).Foreground(t.FgBase),
+ FileSize: base.Foreground(t.FgMuted),
+ EmptyDirectory: base.Foreground(t.FgMuted).PaddingLeft(2).SetString("Empty directory"),
+ },
}
}
@@ -14,6 +14,7 @@ import (
"github.com/opencode-ai/opencode/internal/tui/components/core/status"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs/filepicker"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs/models"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs/sessions"
@@ -125,12 +126,22 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
}
}
+
case commands.SwitchModelMsg:
return a, util.CmdHandler(
dialogs.OpenDialogMsg{
Model: models.NewModelDialogCmp(),
},
)
+ // File Picker
+ case chat.OpenFilePickerMsg:
+ if a.dialog.ActiveDialogId() == filepicker.FilePickerID {
+ // If the commands dialog is already open, close it
+ return a, util.CmdHandler(dialogs.CloseDialogMsg{})
+ }
+ return a, util.CmdHandler(dialogs.OpenDialogMsg{
+ Model: filepicker.NewFilePickerCmp(),
+ })
case tea.KeyPressMsg:
return a, a.handleKeyPressMsg(msg)
}
@@ -138,6 +149,11 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.status = s.(status.StatusCmp)
updated, cmd := a.pages[a.currentPage].Update(msg)
a.pages[a.currentPage] = updated.(util.Model)
+ if a.dialog.HasDialogs() {
+ u, dialogCmd := a.dialog.Update(msg)
+ a.dialog = u.(dialogs.DialogCmp)
+ cmds = append(cmds, dialogCmd)
+ }
cmds = append(cmds, cmd)
return a, tea.Batch(cmds...)
}
@@ -277,6 +293,7 @@ func (a *appModel) View() tea.View {
lipgloss.NewLayer(appView),
}
if a.dialog.HasDialogs() {
+ logging.Info("Rendering dialogs")
layers = append(
layers,
a.dialog.GetLayers()...,