wip filepicker

Kujtim Hoxha created

Change summary

diff.diff                                                | 124 --------
go.mod                                                   |  10 
go.sum                                                   |  23 -
internal/tui/components/dialogs/filepicker/filepicker.go | 141 ++++++++++
internal/tui/components/dialogs/filepicker/keys.go       |  73 +++++
internal/tui/keys.go                                     |  15 
internal/tui/page/chat/chat.go                           |  19 
internal/tui/page/chat/keys.go                           |   5 
internal/tui/styles/theme.go                             |  18 +
internal/tui/tui.go                                      |  17 +
10 files changed, 278 insertions(+), 167 deletions(-)

Detailed changes

diff.diff 🔗

@@ -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{

go.mod 🔗

@@ -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

go.sum 🔗

@@ -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=

internal/tui/components/dialogs/filepicker/filepicker.go 🔗

@@ -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
+}

internal/tui/components/dialogs/filepicker/keys.go 🔗

@@ -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,
+	}
+}

internal/tui/keys.go 🔗

@@ -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"),
-		),
 	}
 }
 

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

@@ -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 == "" {

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

@@ -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"),
+		),
 	}
 }
 

internal/tui/styles/theme.go 🔗

@@ -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"),
+		},
 	}
 }
 

internal/tui/tui.go 🔗

@@ -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()...,