on main help sign in
wip filepicker
commit6188eff8311e51dc2ac4de357fa8a95cc991d2ca authorKujtim Hoxha <kujtimii.h@gmail.com> dateSat 7 Jun 2025, 15:08 (1 year ago) parentc03817c
10 files changed+278−167
diff.diff−124 go.mod+3−7 go.sum+4−19 internal/tui/components/dialogs/filepicker/filepicker.go+141 internal/tui/components/dialogs/filepicker/keys.go+73 internal/tui/keys.go+5−10 internal/tui/page/chat/chat.go+12−7 internal/tui/page/chat/keys.go+5 internal/tui/styles/theme.go+18 internal/tui/tui.go+17
diff.diffdeleted−124
@@ -1,124 +0,0 @@
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+3−7
@@ -11,9 +11,8 @@ require (
1111 github.com/anthropics/anthropic-sdk-go v1.4.0
1212 github.com/aymanbagabas/go-udiff v0.2.0
1313 github.com/bmatcuk/doublestar/v4 v4.8.1
14− github.com/catppuccin/go v0.3.0
1514 github.com/charlievieth/fastwalk v1.0.11
16− github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f
15+ github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
1716 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174
1817 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
1918 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
@@ -23,9 +22,6 @@ require (
2322 github.com/go-logfmt/logfmt v0.6.0
2423 github.com/google/uuid v1.6.0
2524 github.com/mark3labs/mcp-go v0.17.0
26− github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
27− github.com/muesli/reflow v0.3.0
28− github.com/muesli/termenv v0.16.0
2925 github.com/ncruces/go-sqlite3 v0.25.0
3026 github.com/openai/openai-go v0.1.0-beta.2
3127 github.com/pressly/goose/v3 v3.24.2
@@ -37,6 +33,8 @@ require (
3733 github.com/stretchr/testify v1.10.0
3834)
3935
36+require github.com/dustin/go-humanize v1.0.1 // indirect
37+
4038require (
4139 cloud.google.com/go v0.116.0 // indirect
4240 cloud.google.com/go/auth v0.13.0 // indirect
@@ -60,7 +58,6 @@ require (
6058 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
6159 github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
6260 github.com/aws/smithy-go v1.20.3 // indirect
63− github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
6461 github.com/aymerick/douceur v0.2.0 // indirect
6562 github.com/charmbracelet/colorprofile v0.3.1 // indirect
6663 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
@@ -85,7 +82,6 @@ require (
8582 github.com/inconshreveable/mousetrap v1.1.0 // indirect
8683 github.com/kylelemons/godebug v1.1.0 // indirect
8784 github.com/lucasb-eyer/go-colorful v1.2.0
88− github.com/mattn/go-isatty v0.0.20 // indirect
8985 github.com/mattn/go-runewidth v0.0.16 // indirect
9086 github.com/mfridman/interpolate v0.0.2 // indirect
9187 github.com/microcosm-cc/bluemonday v1.0.27 // indirect
go.sum+4−19
@@ -58,24 +58,18 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr
5858github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
5959github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
6060github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
61−github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
62−github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
6361github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
6462github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
6563github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
6664github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
6765github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
6866github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
69−github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
70−github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
7167github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
7268github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
73−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318 h1:f8Q0ybZGxT+St1JfPM7yoz/XFpbmtodcIehaom/9XT8=
74−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
75−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603122936-f1a3fad2b64e h1:+3I/1v7vbN0Vf8Tjm3Q0zdLQqjOM/TjQBvoRDQtoAss=
76−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603122936-f1a3fad2b64e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
77−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f h1:vvNB+i59Wp3L6gYcpuhfAdNjr4/e6qM/st3ySWfmZnU=
78−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
69+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607105053-36addcd3ab8c h1:Dgy7cOR3skvJjGVnhyaixW6ugxVxLtSjRCiMRSdbXSc=
70+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607105053-36addcd3ab8c/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
71+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
72+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
7973github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174 h1:TlVW+df0rdU/osP0O8DIVS9WFOAzXe3nuiMwJR4n+CA=
8074github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
8175github.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
162156github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
163157github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
164158github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
165−github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
166159github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
167160github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
168161github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
169162github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
170163github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
171164github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
172−github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
173−github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
174165github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
175166github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
176−github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
177−github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
178−github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
179−github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
180167github.com/ncruces/go-sqlite3 v0.25.0 h1:trugKUs98Zwy9KwRr/EUxZHL92LYt7UqcKqAfpGpK+I=
181168github.com/ncruces/go-sqlite3 v0.25.0/go.mod h1:n6Z7036yFilJx04yV0mi5JWaF66rUmXn1It9Ux8dx68=
182169github.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
196183github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
197184github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
198185github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
199−github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
200186github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
201187github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
202188github.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
312298golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
313299golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
314300golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
315−golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
316301golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
317302golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
318303golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
internal/tui/components/dialogs/filepicker/filepicker.gonew file+141
@@ -0,0 +1,141 @@
1+package filepicker
2+
3+import (
4+ "os"
5+ "strings"
6+
7+ "github.com/charmbracelet/bubbles/v2/filepicker"
8+ tea "github.com/charmbracelet/bubbletea/v2"
9+ "github.com/charmbracelet/lipgloss/v2"
10+ "github.com/opencode-ai/opencode/internal/tui/components/core"
11+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
12+ "github.com/opencode-ai/opencode/internal/tui/styles"
13+)
14+
15+const (
16+ maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
17+ FilePickerID = "filepicker"
18+ fileSelectionHight = 10
19+)
20+
21+type FilePicker interface {
22+ dialogs.DialogModel
23+}
24+
25+type filePicker struct {
26+ wWidth int
27+ wHeight int
28+ width int
29+ filepicker filepicker.Model
30+ selectedFile string
31+}
32+
33+func NewFilePickerCmp() FilePicker {
34+ t := styles.CurrentTheme()
35+ fp := filepicker.New()
36+ fp.AllowedTypes = []string{".jpg", ".jpeg", ".png"}
37+ fp.CurrentDirectory, _ = os.UserHomeDir()
38+ fp.ShowPermissions = false
39+ fp.ShowSize = false
40+ fp.AutoHeight = false
41+ fp.Styles = t.S().FilePicker
42+ fp.Cursor = ""
43+ fp.SetHeight(fileSelectionHight)
44+
45+ return &filePicker{filepicker: fp}
46+}
47+
48+func (m *filePicker) Init() tea.Cmd {
49+ return m.filepicker.Init()
50+}
51+
52+func (m *filePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
53+ switch msg := msg.(type) {
54+ case tea.WindowSizeMsg:
55+ m.wWidth = msg.Width
56+ m.wHeight = msg.Height
57+ m.width = min(70, m.wWidth)
58+ styles := m.filepicker.Styles
59+ styles.Directory = styles.Directory.Width(m.width - 4)
60+ styles.Selected = styles.Selected.PaddingLeft(1).Width(m.width - 4)
61+ styles.DisabledSelected = styles.DisabledSelected.PaddingLeft(1).Width(m.width - 4)
62+ styles.File = styles.File.Width(m.width)
63+ m.filepicker.Styles = styles
64+ return m, nil
65+ }
66+
67+ var cmd tea.Cmd
68+ m.filepicker, cmd = m.filepicker.Update(msg)
69+
70+ // Did the user select a file?
71+ if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
72+ // Get the path of the selected file.
73+ m.selectedFile = path
74+ }
75+
76+ return m, cmd
77+}
78+
79+func (m *filePicker) View() tea.View {
80+ t := styles.CurrentTheme()
81+
82+ content := lipgloss.JoinVertical(
83+ lipgloss.Left,
84+ t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Add Image", m.width-4)),
85+ m.imagePreview(),
86+ m.filepicker.View(),
87+ )
88+ return tea.NewView(m.style().Render(content))
89+}
90+
91+func (m *filePicker) currentImage() string {
92+ for _, ext := range m.filepicker.AllowedTypes {
93+ if strings.HasSuffix(m.filepicker.HighlightedPath(), ext) {
94+ return m.filepicker.HighlightedPath()
95+ }
96+ }
97+ return ""
98+}
99+
100+func (m *filePicker) imagePreview() string {
101+ if m.currentImage() == "" {
102+ return m.imagePreviewStyle().Render()
103+ }
104+
105+ return ""
106+}
107+
108+func (m *filePicker) imagePreviewStyle() lipgloss.Style {
109+ t := styles.CurrentTheme()
110+ w, h := m.imagePreviewSize()
111+ return t.S().Base.
112+ Width(w).
113+ Height(h).
114+ Margin(1).
115+ Background(t.BgOverlay)
116+}
117+
118+func (m *filePicker) imagePreviewSize() (int, int) {
119+ return m.width - 4, min(20, m.wHeight/2)
120+}
121+
122+func (m *filePicker) style() lipgloss.Style {
123+ t := styles.CurrentTheme()
124+ return t.S().Base.
125+ Width(m.width).
126+ Border(lipgloss.RoundedBorder()).
127+ BorderForeground(t.BorderFocus)
128+}
129+
130+// ID implements FilePicker.
131+func (m *filePicker) ID() dialogs.DialogID {
132+ return FilePickerID
133+}
134+
135+// Position implements FilePicker.
136+func (m *filePicker) Position() (int, int) {
137+ row := m.wHeight/4 - 2 // just a bit above the center
138+ col := m.wWidth / 2
139+ col -= m.width / 2
140+ return row, col
141+}
internal/tui/components/dialogs/filepicker/keys.gonew file+73
@@ -0,0 +1,73 @@
1+package filepicker
2+
3+import (
4+ "github.com/charmbracelet/bubbles/v2/key"
5+ "github.com/opencode-ai/opencode/internal/tui/layout"
6+)
7+
8+// KeyMap defines keyboard bindings for dialog management.
9+type KeyMap struct {
10+ Select,
11+ Down,
12+ Up,
13+ Forward,
14+ Backward,
15+ InsertCWD,
16+ Close key.Binding
17+}
18+
19+func DefaultKeyMap() KeyMap {
20+ return KeyMap{
21+ Select: key.NewBinding(
22+ key.WithKeys("enter"),
23+ key.WithHelp("enter", "accept"),
24+ ),
25+ Down: key.NewBinding(
26+ key.WithKeys("down", "j"),
27+ key.WithHelp("down/j", "move down"),
28+ ),
29+ Up: key.NewBinding(
30+ key.WithKeys("up", "k"),
31+ key.WithHelp("up/k", "move up"),
32+ ),
33+ Forward: key.NewBinding(
34+ key.WithKeys("right", "l"),
35+ key.WithHelp("right/l", "move forward"),
36+ ),
37+ Backward: key.NewBinding(
38+ key.WithKeys("left", "h"),
39+ key.WithHelp("left/h", "move backward"),
40+ ),
41+ InsertCWD: key.NewBinding(
42+ key.WithKeys("i"),
43+ key.WithHelp("i", "manual path input"),
44+ ),
45+ Close: key.NewBinding(
46+ key.WithKeys("esc"),
47+ key.WithHelp("esc", "close/exit"),
48+ ),
49+ }
50+}
51+
52+// FullHelp implements help.KeyMap.
53+func (k KeyMap) FullHelp() [][]key.Binding {
54+ m := [][]key.Binding{}
55+ slice := layout.KeyMapToSlice(k)
56+ for i := 0; i < len(slice); i += 4 {
57+ end := min(i+4, len(slice))
58+ m = append(m, slice[i:end])
59+ }
60+ return m
61+}
62+
63+// ShortHelp implements help.KeyMap.
64+func (k KeyMap) ShortHelp() []key.Binding {
65+ return []key.Binding{
66+ k.InsertCWD,
67+ key.NewBinding(
68+ key.WithHelp("↑↓←→", "navigate"),
69+ ),
70+ k.Select,
71+ k.Close,
72+ }
73+}
internal/tui/keys.go+5−10
@@ -6,12 +6,11 @@ import (
66)
77
88type KeyMap struct {
9− Logs key.Binding
10− Quit key.Binding
11− Help key.Binding
12− Commands key.Binding
13− Sessions key.Binding
14− FilePicker key.Binding
9+ Logs key.Binding
10+ Quit key.Binding
11+ Help key.Binding
12+ Commands key.Binding
13+ Sessions key.Binding
1514}
1615
1716func DefaultKeyMap() KeyMap {
@@ -37,10 +36,6 @@ func DefaultKeyMap() KeyMap {
3736 key.WithKeys("ctrl+s"),
3837 key.WithHelp("ctrl+s", "sessions"),
3938 ),
40− FilePicker: key.NewBinding(
41− key.WithKeys("ctrl+f"),
42− key.WithHelp("ctrl+f", "select files to upload"),
43− ),
4439 }
4540}
4641
internal/tui/page/chat/chat.go+12−7
@@ -24,17 +24,20 @@ type ChatFocusedMsg struct {
2424 Focused bool // True if the chat input is focused, false otherwise
2525}
2626
27−type chatPage struct {
28− app *app.App
27+type (
28+ OpenFilePickerMsg struct{}
29+ chatPage struct {
30+ app *app.App
2931
30− layout layout.SplitPaneLayout
32+ layout layout.SplitPaneLayout
3133
32− session session.Session
34+ session session.Session
3335
34− keyMap KeyMap
36+ keyMap KeyMap
3537
36− chatFocused bool
37−}
38+ chatFocused bool
39+ }
40+)
3841
3942func (p *chatPage) Init() tea.Cmd {
4043 cmd := p.layout.Init()
@@ -83,6 +86,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8386 util.CmdHandler(chat.SessionClearedMsg{}),
8487 )
8588
89+ case key.Matches(msg, p.keyMap.FilePicker):
90+ return p, util.CmdHandler(OpenFilePickerMsg{})
8691 case key.Matches(msg, p.keyMap.Tab):
8792 logging.Info("Tab key pressed, toggling chat focus")
8893 if p.session.ID == "" {
internal/tui/page/chat/keys.go+5
@@ -7,6 +7,7 @@ import (
77
88type KeyMap struct {
99 NewSession key.Binding
10+ FilePicker key.Binding
1011 Cancel key.Binding
1112 Tab key.Binding
1213}
@@ -25,6 +26,10 @@ func DefaultKeyMap() KeyMap {
2526 key.WithKeys("tab"),
2627 key.WithHelp("tab", "change focus"),
2728 ),
29+ FilePicker: key.NewBinding(
30+ key.WithKeys("ctrl+f"),
31+ key.WithHelp("ctrl+f", "select files to upload"),
32+ ),
2833 }
2934}
3035
internal/tui/styles/theme.go+18
@@ -5,6 +5,7 @@ import (
55 "image/color"
66 "strings"
77
8+ "github.com/charmbracelet/bubbles/v2/filepicker"
89 "github.com/charmbracelet/bubbles/v2/help"
910 "github.com/charmbracelet/bubbles/v2/textarea"
1011 "github.com/charmbracelet/bubbles/v2/textinput"
@@ -110,6 +111,9 @@ type Styles struct {
110111
111112 // Diff
112113 Diff Diff
114+
115+ // FilePicker
116+ FilePicker filepicker.Styles
113117}
114118
115119func (t *Theme) S() *Styles {
@@ -430,6 +434,20 @@ func (t *Theme) buildStyles() *Styles {
430434 AddedLineNumberBg: t.GreenDark,
431435 RemovedLineNumberBg: t.RedDark,
432436 },
437+
438+ FilePicker: filepicker.Styles{
439+ DisabledCursor: base.Foreground(t.FgMuted),
440+ Cursor: base.Foreground(t.FgBase),
441+ Symlink: base.Foreground(t.FgSubtle),
442+ Directory: base.Foreground(t.Primary),
443+ File: base.Foreground(t.FgBase),
444+ DisabledFile: base.Foreground(t.FgMuted),
445+ DisabledSelected: base.Background(t.BgOverlay).Foreground(t.FgMuted),
446+ Permission: base.Foreground(t.FgMuted),
447+ Selected: base.Background(t.Primary).Foreground(t.FgBase),
448+ FileSize: base.Foreground(t.FgMuted),
449+ EmptyDirectory: base.Foreground(t.FgMuted).PaddingLeft(2).SetString("Empty directory"),
450+ },
433451 }
434452}
435453
internal/tui/tui.go+17
@@ -14,6 +14,7 @@ import (
1414 "github.com/opencode-ai/opencode/internal/tui/components/core/status"
1515 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
1616 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
17+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs/filepicker"
1718 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/models"
1819 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
1920 "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) {
125126 Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
126127 }
127128 }
129+
128130 case commands.SwitchModelMsg:
129131 return a, util.CmdHandler(
130132 dialogs.OpenDialogMsg{
131133 Model: models.NewModelDialogCmp(),
132134 },
133135 )
136+ // File Picker
137+ case chat.OpenFilePickerMsg:
138+ if a.dialog.ActiveDialogId() == filepicker.FilePickerID {
139+ // If the commands dialog is already open, close it
140+ return a, util.CmdHandler(dialogs.CloseDialogMsg{})
141+ }
142+ return a, util.CmdHandler(dialogs.OpenDialogMsg{
143+ Model: filepicker.NewFilePickerCmp(),
144+ })
134145 case tea.KeyPressMsg:
135146 return a, a.handleKeyPressMsg(msg)
136147 }
@@ -138,6 +149,11 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
138149 a.status = s.(status.StatusCmp)
139150 updated, cmd := a.pages[a.currentPage].Update(msg)
140151 a.pages[a.currentPage] = updated.(util.Model)
152+ if a.dialog.HasDialogs() {
153+ u, dialogCmd := a.dialog.Update(msg)
154+ a.dialog = u.(dialogs.DialogCmp)
155+ cmds = append(cmds, dialogCmd)
156+ }
141157 cmds = append(cmds, cmd)
142158 return a, tea.Batch(cmds...)
143159}
@@ -277,6 +293,7 @@ func (a *appModel) View() tea.View {
277293 lipgloss.NewLayer(appView),
278294 }
279295 if a.dialog.HasDialogs() {
296+ logging.Info("Rendering dialogs")
280297 layers = append(
281298 layers,
282299 a.dialog.GetLayers()...,