on main help sign in
wip filepicker
6188effKujtim Hoxha
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 (
11 github.com/anthropics/anthropic-sdk-go v1.4.0
11 github.com/anthropics/anthropic-sdk-go v1.4.0
12 github.com/aymanbagabas/go-udiff v0.2.0
12 github.com/aymanbagabas/go-udiff v0.2.0
13 github.com/bmatcuk/doublestar/v4 v4.8.1
13 github.com/bmatcuk/doublestar/v4 v4.8.1
14− github.com/catppuccin/go v0.3.0
15 github.com/charlievieth/fastwalk v1.0.11
14 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
17 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174
16 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174
18 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
17 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
19 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
18 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
@@ -23,9 +22,6 @@ require (
23 github.com/go-logfmt/logfmt v0.6.0
22 github.com/go-logfmt/logfmt v0.6.0
24 github.com/google/uuid v1.6.0
23 github.com/google/uuid v1.6.0
25 github.com/mark3labs/mcp-go v0.17.0
24 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
29 github.com/ncruces/go-sqlite3 v0.25.0
25 github.com/ncruces/go-sqlite3 v0.25.0
30 github.com/openai/openai-go v0.1.0-beta.2
26 github.com/openai/openai-go v0.1.0-beta.2
31 github.com/pressly/goose/v3 v3.24.2
27 github.com/pressly/goose/v3 v3.24.2
@@ -37,6 +33,8 @@ require (
37 github.com/stretchr/testify v1.10.0
33 github.com/stretchr/testify v1.10.0
38)
34)
39
35
36+require github.com/dustin/go-humanize v1.0.1 // indirect
37+
40require (
38require (
41 cloud.google.com/go v0.116.0 // indirect
39 cloud.google.com/go v0.116.0 // indirect
42 cloud.google.com/go/auth v0.13.0 // indirect
40 cloud.google.com/go/auth v0.13.0 // indirect
@@ -60,7 +58,6 @@ require (
60 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
58 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
61 github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
59 github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
62 github.com/aws/smithy-go v1.20.3 // indirect
60 github.com/aws/smithy-go v1.20.3 // indirect
63− github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
64 github.com/aymerick/douceur v0.2.0 // indirect
61 github.com/aymerick/douceur v0.2.0 // indirect
65 github.com/charmbracelet/colorprofile v0.3.1 // indirect
62 github.com/charmbracelet/colorprofile v0.3.1 // indirect
66 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
63 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
@@ -85,7 +82,6 @@ require (
85 github.com/inconshreveable/mousetrap v1.1.0 // indirect
82 github.com/inconshreveable/mousetrap v1.1.0 // indirect
86 github.com/kylelemons/godebug v1.1.0 // indirect
83 github.com/kylelemons/godebug v1.1.0 // indirect
87 github.com/lucasb-eyer/go-colorful v1.2.0
84 github.com/lucasb-eyer/go-colorful v1.2.0
88− github.com/mattn/go-isatty v0.0.20 // indirect
89 github.com/mattn/go-runewidth v0.0.16 // indirect
85 github.com/mattn/go-runewidth v0.0.16 // indirect
90 github.com/mfridman/interpolate v0.0.2 // indirect
86 github.com/mfridman/interpolate v0.0.2 // indirect
91 github.com/microcosm-cc/bluemonday v1.0.27 // indirect
87 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
58github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
58github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
59github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
59github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
60github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
60github.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=
63github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
61github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
64github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
62github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
65github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
63github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
66github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
64github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
67github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
65github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
68github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
66github.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=
71github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
67github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
72github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
68github.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=
69+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607105053-36addcd3ab8c h1:Dgy7cOR3skvJjGVnhyaixW6ugxVxLtSjRCiMRSdbXSc=
74−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
70+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607105053-36addcd3ab8c/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=
71+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
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=
72+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
79github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174 h1:TlVW+df0rdU/osP0O8DIVS9WFOAzXe3nuiMwJR4n+CA=
73github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174 h1:TlVW+df0rdU/osP0O8DIVS9WFOAzXe3nuiMwJR4n+CA=
80github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
74github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
81github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
75github.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
162github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
156github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
163github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
157github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
164github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
158github.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=
166github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
159github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
167github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
160github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
168github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
161github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
169github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
162github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
170github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
163github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
171github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
164github.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=
174github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
165github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
175github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
166github.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=
180github.com/ncruces/go-sqlite3 v0.25.0 h1:trugKUs98Zwy9KwRr/EUxZHL92LYt7UqcKqAfpGpK+I=
167github.com/ncruces/go-sqlite3 v0.25.0 h1:trugKUs98Zwy9KwRr/EUxZHL92LYt7UqcKqAfpGpK+I=
181github.com/ncruces/go-sqlite3 v0.25.0/go.mod h1:n6Z7036yFilJx04yV0mi5JWaF66rUmXn1It9Ux8dx68=
168github.com/ncruces/go-sqlite3 v0.25.0/go.mod h1:n6Z7036yFilJx04yV0mi5JWaF66rUmXn1It9Ux8dx68=
182github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
169github.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
196github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
183github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
197github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
184github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
198github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
185github.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=
200github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
186github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
201github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
187github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
202github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
188github.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
312golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
298golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
313golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
299golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
314golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
300golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
315−golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
316golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
301golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
317golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
302golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
318golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
303golang.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 (
6)
6)
7
7
8type KeyMap struct {
8type KeyMap struct {
9− Logs key.Binding
9+ Logs key.Binding
10− Quit key.Binding
10+ Quit key.Binding
11− Help key.Binding
11+ Help key.Binding
12− Commands key.Binding
12+ Commands key.Binding
13− Sessions key.Binding
14− FilePicker key.Binding
13+ Sessions key.Binding
15}
14}
16
15
17func DefaultKeyMap() KeyMap {
16func DefaultKeyMap() KeyMap {
@@ -37,10 +36,6 @@ func DefaultKeyMap() KeyMap {
37 key.WithKeys("ctrl+s"),
36 key.WithKeys("ctrl+s"),
38 key.WithHelp("ctrl+s", "sessions"),
37 key.WithHelp("ctrl+s", "sessions"),
39 ),
38 ),
40− FilePicker: key.NewBinding(
41− key.WithKeys("ctrl+f"),
42− key.WithHelp("ctrl+f", "select files to upload"),
43− ),
44 }
39 }
45}
40}
46
41
internal/tui/page/chat/chat.go+12−7
@@ -24,17 +24,20 @@ type ChatFocusedMsg struct {
24 Focused bool // True if the chat input is focused, false otherwise
24 Focused bool // True if the chat input is focused, false otherwise
25}
25}
26
26
27+type (
28+ OpenFilePickerMsg struct{}
27−type chatPage struct {
29+ chatPage struct {
28− app *app.App
30+ app *app.App
29
31
30− layout layout.SplitPaneLayout
32+ layout layout.SplitPaneLayout
31
33
32− session session.Session
34+ session session.Session
33
35
34− keyMap KeyMap
36+ keyMap KeyMap
35
37
36− chatFocused bool
38+ chatFocused bool
37−}
39+ }
40+)
38
41
39func (p *chatPage) Init() tea.Cmd {
42func (p *chatPage) Init() tea.Cmd {
40 cmd := p.layout.Init()
43 cmd := p.layout.Init()
@@ -83,6 +86,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
83 util.CmdHandler(chat.SessionClearedMsg{}),
86 util.CmdHandler(chat.SessionClearedMsg{}),
84 )
87 )
85
88
89+ case key.Matches(msg, p.keyMap.FilePicker):
90+ return p, util.CmdHandler(OpenFilePickerMsg{})
86 case key.Matches(msg, p.keyMap.Tab):
91 case key.Matches(msg, p.keyMap.Tab):
87 logging.Info("Tab key pressed, toggling chat focus")
92 logging.Info("Tab key pressed, toggling chat focus")
88 if p.session.ID == "" {
93 if p.session.ID == "" {
internal/tui/page/chat/keys.go+5
@@ -7,6 +7,7 @@ import (
7
7
8type KeyMap struct {
8type KeyMap struct {
9 NewSession key.Binding
9 NewSession key.Binding
10+ FilePicker key.Binding
10 Cancel key.Binding
11 Cancel key.Binding
11 Tab key.Binding
12 Tab key.Binding
12}
13}
@@ -25,6 +26,10 @@ func DefaultKeyMap() KeyMap {
25 key.WithKeys("tab"),
26 key.WithKeys("tab"),
26 key.WithHelp("tab", "change focus"),
27 key.WithHelp("tab", "change focus"),
27 ),
28 ),
29+ FilePicker: key.NewBinding(
30+ key.WithKeys("ctrl+f"),
31+ key.WithHelp("ctrl+f", "select files to upload"),
32+ ),
28 }
33 }
29}
34}
30
35
internal/tui/styles/theme.go+18
@@ -5,6 +5,7 @@ import (
5 "image/color"
5 "image/color"
6 "strings"
6 "strings"
7
7
8+ "github.com/charmbracelet/bubbles/v2/filepicker"
8 "github.com/charmbracelet/bubbles/v2/help"
9 "github.com/charmbracelet/bubbles/v2/help"
9 "github.com/charmbracelet/bubbles/v2/textarea"
10 "github.com/charmbracelet/bubbles/v2/textarea"
10 "github.com/charmbracelet/bubbles/v2/textinput"
11 "github.com/charmbracelet/bubbles/v2/textinput"
@@ -110,6 +111,9 @@ type Styles struct {
110
111
111 // Diff
112 // Diff
112 Diff Diff
113 Diff Diff
114+
115+ // FilePicker
116+ FilePicker filepicker.Styles
113}
117}
114
118
115func (t *Theme) S() *Styles {
119func (t *Theme) S() *Styles {
@@ -430,6 +434,20 @@ func (t *Theme) buildStyles() *Styles {
430 AddedLineNumberBg: t.GreenDark,
434 AddedLineNumberBg: t.GreenDark,
431 RemovedLineNumberBg: t.RedDark,
435 RemovedLineNumberBg: t.RedDark,
432 },
436 },
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+ },
433 }
451 }
434}
452}
435
453
internal/tui/tui.go+17
@@ -14,6 +14,7 @@ import (
14 "github.com/opencode-ai/opencode/internal/tui/components/core/status"
14 "github.com/opencode-ai/opencode/internal/tui/components/core/status"
15 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
15 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
16 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
16 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
17+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs/filepicker"
17 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/models"
18 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/models"
18 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
19 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
19 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/sessions"
20 "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) {
125 Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
126 Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
126 }
127 }
127 }
128 }
129+
128 case commands.SwitchModelMsg:
130 case commands.SwitchModelMsg:
129 return a, util.CmdHandler(
131 return a, util.CmdHandler(
130 dialogs.OpenDialogMsg{
132 dialogs.OpenDialogMsg{
131 Model: models.NewModelDialogCmp(),
133 Model: models.NewModelDialogCmp(),
132 },
134 },
133 )
135 )
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+ })
134 case tea.KeyPressMsg:
145 case tea.KeyPressMsg:
135 return a, a.handleKeyPressMsg(msg)
146 return a, a.handleKeyPressMsg(msg)
136 }
147 }
@@ -138,6 +149,11 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
138 a.status = s.(status.StatusCmp)
149 a.status = s.(status.StatusCmp)
139 updated, cmd := a.pages[a.currentPage].Update(msg)
150 updated, cmd := a.pages[a.currentPage].Update(msg)
140 a.pages[a.currentPage] = updated.(util.Model)
151 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+ }
141 cmds = append(cmds, cmd)
157 cmds = append(cmds, cmd)
142 return a, tea.Batch(cmds...)
158 return a, tea.Batch(cmds...)
143}
159}
@@ -277,6 +293,7 @@ func (a *appModel) View() tea.View {
277 lipgloss.NewLayer(appView),
293 lipgloss.NewLayer(appView),
278 }
294 }
279 if a.dialog.HasDialogs() {
295 if a.dialog.HasDialogs() {
296+ logging.Info("Rendering dialogs")
280 layers = append(
297 layers = append(
281 layers,
298 layers,
282 a.dialog.GetLayers()...,
299 a.dialog.GetLayers()...,