summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-08 08:08:53 -0500
committeradamdottv <[email protected]>2025-07-08 08:09:01 -0500
commit662d022a4859ee1c004133559ee42c5f7044dda7 (patch)
tree304b17deceb254d0d70d29e9d46e865b1c4868cf
parent9efef03919f99750277a7f15722c24d7c7958224 (diff)
downloadopencode-662d022a4859ee1c004133559ee42c5f7044dda7.tar.gz
opencode-662d022a4859ee1c004133559ee42c5f7044dda7.zip
feat(tui): paste images and pdfs
-rw-r--r--package.json2
-rw-r--r--packages/opencode/src/config/config.ts12
-rw-r--r--packages/tui/cmd/opencode/main.go8
-rw-r--r--packages/tui/go.mod22
-rw-r--r--packages/tui/go.sum41
-rw-r--r--packages/tui/internal/app/app.go12
-rw-r--r--packages/tui/internal/commands/command.go2
-rw-r--r--packages/tui/internal/components/chat/editor.go93
-rw-r--r--packages/tui/internal/components/textarea/textarea.go26
-rw-r--r--packages/tui/internal/image/clipboard_unix.go46
-rw-r--r--packages/tui/internal/image/clipboard_windows.go192
-rw-r--r--packages/tui/internal/image/images.go86
-rw-r--r--packages/tui/internal/tui/tui.go4
-rw-r--r--packages/tui/sdk/.stats.yml4
-rw-r--r--packages/tui/sdk/config.go36
-rwxr-xr-xpackages/tui/sdk/scripts/lint5
16 files changed, 182 insertions, 409 deletions
diff --git a/package.json b/package.json
index 2a0038e0c..7a8c6b071 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"scripts": {
"dev": "bun run packages/opencode/src/index.ts",
"typecheck": "bun run --filter='*' typecheck",
- "stainless": "bun run ./packages/opencode/src/index.ts serve ",
+ "stainless": "./scripts/stainless",
"postinstall": "./scripts/hooks"
},
"workspaces": {
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index c90c951d4..c43a382a3 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -58,23 +58,26 @@ export namespace Config {
export const Keybinds = z
.object({
leader: z.string().optional().describe("Leader key for keybind combinations"),
- help: z.string().optional().describe("Show help dialog"),
+ app_help: z.string().optional().describe("Show help dialog"),
editor_open: z.string().optional().describe("Open external editor"),
session_new: z.string().optional().describe("Create a new session"),
session_list: z.string().optional().describe("List all sessions"),
session_share: z.string().optional().describe("Share current session"),
+ session_unshare: z.string().optional().describe("Unshare current session"),
session_interrupt: z.string().optional().describe("Interrupt current session"),
session_compact: z.string().optional().describe("Toggle compact mode for session"),
tool_details: z.string().optional().describe("Show tool details"),
model_list: z.string().optional().describe("List available models"),
theme_list: z.string().optional().describe("List available themes"),
+ file_list: z.string().optional().describe("List files"),
+ file_close: z.string().optional().describe("Close file"),
+ file_search: z.string().optional().describe("Search file"),
+ file_diff_toggle: z.string().optional().describe("Toggle split/unified diff"),
project_init: z.string().optional().describe("Initialize project configuration"),
input_clear: z.string().optional().describe("Clear input field"),
input_paste: z.string().optional().describe("Paste from clipboard"),
input_submit: z.string().optional().describe("Submit input"),
input_newline: z.string().optional().describe("Insert newline in input"),
- history_previous: z.string().optional().describe("Navigate to previous history item"),
- history_next: z.string().optional().describe("Navigate to next history item"),
messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
@@ -83,6 +86,9 @@ export namespace Config {
messages_next: z.string().optional().describe("Navigate to next message"),
messages_first: z.string().optional().describe("Navigate to first message"),
messages_last: z.string().optional().describe("Navigate to last message"),
+ messages_layout_toggle: z.string().optional().describe("Toggle layout"),
+ messages_copy: z.string().optional().describe("Copy message"),
+ messages_revert: z.string().optional().describe("Revert message"),
app_exit: z.string().optional().describe("Exit the application"),
})
.strict()
diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go
index e456a3edc..ada1880d1 100644
--- a/packages/tui/cmd/opencode/main.go
+++ b/packages/tui/cmd/opencode/main.go
@@ -14,6 +14,7 @@ import (
"github.com/sst/opencode-sdk-go/option"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/tui"
+ "golang.design/x/clipboard"
)
var Version = "dev"
@@ -66,6 +67,13 @@ func main() {
os.Exit(1)
}
+ go func() {
+ err = clipboard.Init()
+ if err != nil {
+ slog.Error("Failed to initialize clipboard", "error", err)
+ }
+ }()
+
// Create main context for the application
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
diff --git a/packages/tui/go.mod b/packages/tui/go.mod
index a32dd46be..10f6e7e49 100644
--- a/packages/tui/go.mod
+++ b/packages/tui/go.mod
@@ -17,6 +17,7 @@ require (
github.com/muesli/termenv v0.16.0
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
+ golang.design/x/clipboard v0.7.1
rsc.io/qr v0.2.0
)
@@ -54,8 +55,10 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
- golang.org/x/mod v0.24.0 // indirect
- golang.org/x/tools v0.31.0 // indirect
+ golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b // indirect
+ golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
+ golang.org/x/mod v0.25.0 // indirect
+ golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
@@ -66,7 +69,6 @@ require (
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
- github.com/disintegration/imaging v1.6.2
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
@@ -78,16 +80,16 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7
github.com/rogpeppe/go-internal v1.14.1 // indirect
- github.com/spf13/pflag v1.0.6 // indirect
+ github.com/spf13/pflag v1.0.6
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
- golang.org/x/image v0.26.0
- golang.org/x/net v0.39.0 // indirect
- golang.org/x/sync v0.13.0 // indirect
- golang.org/x/sys v0.32.0 // indirect
- golang.org/x/term v0.31.0 // indirect
- golang.org/x/text v0.24.0
+ golang.org/x/image v0.28.0 // indirect
+ golang.org/x/net v0.41.0 // indirect
+ golang.org/x/sync v0.15.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/term v0.32.0 // indirect
+ golang.org/x/text v0.26.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/packages/tui/go.sum b/packages/tui/go.sum
index fdc5bbb01..f35417110 100644
--- a/packages/tui/go.sum
+++ b/packages/tui/go.sum
@@ -54,8 +54,6 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
-github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
@@ -212,20 +210,25 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
+golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
+golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
-golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
-golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
+golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b h1:zELBzk+7ERc6m8BxhzU2VYjp03wlEvi+cIgYQR5H3CI=
+golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
+golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
+golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
+golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8=
+golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
-golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
+golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -236,15 +239,15 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
-golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
-golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -263,28 +266,28 @@ 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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
-golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
-golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
+golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
-golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
+golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 08a999f14..237ee0fee 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -18,6 +18,7 @@ import (
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
+ "golang.design/x/clipboard"
)
type App struct {
@@ -146,6 +147,17 @@ func (a *App) Key(commandName commands.CommandName) string {
return base(key) + muted(" "+command.Description)
}
+func (a *App) SetClipboard(text string) tea.Cmd {
+ var cmds []tea.Cmd
+ cmds = append(cmds, func() tea.Msg {
+ clipboard.Write(clipboard.FmtText, []byte(text))
+ return nil
+ })
+ // try to set the clipboard using OSC52 for terminals that support it
+ cmds = append(cmds, tea.SetClipboard(text))
+ return tea.Sequence(cmds...)
+}
+
func (a *App) InitializeProvider() tea.Cmd {
providersResponse, err := a.Client.Config.Providers(context.Background())
if err != nil {
diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go
index 9c4da12ec..10b0d7e27 100644
--- a/packages/tui/internal/commands/command.go
+++ b/packages/tui/internal/commands/command.go
@@ -231,7 +231,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
{
Name: InputPasteCommand,
Description: "paste content",
- Keybindings: parseBindings("ctrl+v"),
+ Keybindings: parseBindings("ctrl+v", "super+v"),
},
{
Name: InputSubmitCommand,
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index f32e5c32a..cc31fbef2 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -1,9 +1,12 @@
package chat
import (
+ "encoding/base64"
"fmt"
"log/slog"
+ "os"
"path/filepath"
+ "strconv"
"strings"
"github.com/charmbracelet/bubbles/v2/spinner"
@@ -15,10 +18,10 @@ import (
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/components/textarea"
- "github.com/sst/opencode/internal/image"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
+ "golang.design/x/clipboard"
)
type EditorComponent interface {
@@ -63,6 +66,57 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
+ case tea.PasteMsg:
+ text := string(msg)
+ text = strings.ReplaceAll(text, "\\", "")
+ text, err := strconv.Unquote(`"` + text + `"`)
+ if err != nil {
+ slog.Error("Failed to unquote text", "error", err)
+ m.textarea.InsertRunesFromUserInput([]rune(msg))
+ return m, nil
+ }
+ if _, err := os.Stat(text); err != nil {
+ slog.Error("Failed to paste file", "error", err)
+ m.textarea.InsertRunesFromUserInput([]rune(msg))
+ return m, nil
+ }
+
+ filePath := text
+ ext := strings.ToLower(filepath.Ext(filePath))
+
+ mediaType := ""
+ switch ext {
+ case ".jpg":
+ mediaType = "image/jpeg"
+ case ".png", ".jpeg", ".gif", ".webp":
+ mediaType = "image/" + ext[1:]
+ case ".pdf":
+ mediaType = "application/pdf"
+ default:
+ mediaType = "text/plain"
+ }
+
+ fileBytes, err := os.ReadFile(filePath)
+ if err != nil {
+ slog.Error("Failed to read file", "error", err)
+ m.textarea.InsertRunesFromUserInput([]rune(msg))
+ return m, nil
+ }
+ base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
+ url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
+
+ attachment := &textarea.Attachment{
+ ID: uuid.NewString(),
+ Display: fmt.Sprintf("<%s>", filePath),
+ URL: url,
+ Filename: filePath,
+ MediaType: mediaType,
+ }
+ m.textarea.InsertAttachment(attachment)
+ m.textarea.InsertString(" ")
+ case tea.ClipboardMsg:
+ text := string(msg)
+ m.textarea.InsertRunesFromUserInput([]rune(text))
case dialog.ThemeSelectedMsg:
m.textarea = m.resetTextareaStyles()
m.spinner = createSpinner()
@@ -269,24 +323,29 @@ func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
}
func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
- _, text, err := image.GetImageFromClipboard()
- if err != nil {
- slog.Error(err.Error())
+ imageBytes := clipboard.Read(clipboard.FmtImage)
+ if imageBytes != nil {
+ base64EncodedFile := base64.StdEncoding.EncodeToString(imageBytes)
+ attachment := &textarea.Attachment{
+ ID: uuid.NewString(),
+ Display: "<clipboard-image>",
+ Filename: "clipboard-image",
+ MediaType: "image/png",
+ URL: fmt.Sprintf("data:image/png;base64,%s", base64EncodedFile),
+ }
+ m.textarea.InsertAttachment(attachment)
+ m.textarea.InsertString(" ")
return m, nil
}
- // if len(imageBytes) != 0 {
- // attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
- // attachment := app.Attachment{
- // FilePath: attachmentName,
- // FileName: attachmentName,
- // Content: imageBytes,
- // MimeType: "image/png",
- // }
- // m.attachments = append(m.attachments, attachment)
- // } else {
- m.textarea.InsertString(text)
- // }
- return m, nil
+
+ textBytes := clipboard.Read(clipboard.FmtText)
+ if textBytes != nil {
+ m.textarea.InsertRunesFromUserInput([]rune(string(textBytes)))
+ return m, nil
+ }
+
+ // fallback to reading the clipboard using OSC52
+ return m, tea.ReadClipboard
}
func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
diff --git a/packages/tui/internal/components/textarea/textarea.go b/packages/tui/internal/components/textarea/textarea.go
index 5ff936f17..41a4a3d91 100644
--- a/packages/tui/internal/components/textarea/textarea.go
+++ b/packages/tui/internal/components/textarea/textarea.go
@@ -11,7 +11,6 @@ import (
"slices"
- "github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/v2/cursor"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
@@ -653,12 +652,12 @@ func (m *Model) SetValue(s string) {
// InsertString inserts a string at the cursor position.
func (m *Model) InsertString(s string) {
- m.insertRunesFromUserInput([]rune(s))
+ m.InsertRunesFromUserInput([]rune(s))
}
// InsertRune inserts a rune at the cursor position.
func (m *Model) InsertRune(r rune) {
- m.insertRunesFromUserInput([]rune{r})
+ m.InsertRunesFromUserInput([]rune{r})
}
// InsertAttachment inserts an attachment at the cursor position.
@@ -730,8 +729,8 @@ func (m Model) GetAttachments() []*Attachment {
return attachments
}
-// insertRunesFromUserInput inserts runes at the current cursor position.
-func (m *Model) insertRunesFromUserInput(runes []rune) {
+// InsertRunesFromUserInput inserts runes at the current cursor position.
+func (m *Model) InsertRunesFromUserInput(runes []rune) {
// Clean up any special characters in the input provided by the
// clipboard. This avoids bugs due to e.g. tab characters and
// whatnot.
@@ -1429,8 +1428,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
switch msg := msg.(type) {
- case tea.PasteMsg:
- m.insertRunesFromUserInput([]rune(msg))
case tea.KeyPressMsg:
switch {
case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
@@ -1490,8 +1487,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.CursorDown()
case key.Matches(msg, m.KeyMap.WordForward):
m.wordRight()
- case key.Matches(msg, m.KeyMap.Paste):
- return m, Paste
case key.Matches(msg, m.KeyMap.CharacterBackward):
m.characterLeft(false /* insideLine */)
case key.Matches(msg, m.KeyMap.LinePrevious):
@@ -1512,11 +1507,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.transposeLeft()
default:
- m.insertRunesFromUserInput([]rune(msg.Text))
+ m.InsertRunesFromUserInput([]rune(msg.Text))
}
case pasteMsg:
- m.insertRunesFromUserInput([]rune(msg))
+ m.InsertRunesFromUserInput([]rune(msg))
case pasteErrMsg:
m.Err = msg
@@ -1908,15 +1903,6 @@ func (m *Model) splitLine(row, col int) {
m.row++
}
-// Paste is a command for pasting from the clipboard into the text input.
-func Paste() tea.Msg {
- str, err := clipboard.ReadAll()
- if err != nil {
- return pasteErrMsg{err}
- }
- return pasteMsg(str)
-}
-
func wrapInterfaces(content []any, width int) [][]any {
if width <= 0 {
return [][]any{content}
diff --git a/packages/tui/internal/image/clipboard_unix.go b/packages/tui/internal/image/clipboard_unix.go
deleted file mode 100644
index 2653d8cad..000000000
--- a/packages/tui/internal/image/clipboard_unix.go
+++ /dev/null
@@ -1,46 +0,0 @@
-//go:build !windows
-
-package image
-
-import (
- "bytes"
- "fmt"
- "github.com/atotto/clipboard"
- "image"
-)
-
-func GetImageFromClipboard() ([]byte, string, error) {
- text, err := clipboard.ReadAll()
- if err != nil {
- return nil, "", fmt.Errorf("Error reading clipboard")
- }
-
- if text == "" {
- return nil, "", nil
- }
-
- binaryData := []byte(text)
- imageBytes, err := binaryToImage(binaryData)
- if err != nil {
- return nil, text, nil
- }
- return imageBytes, "", nil
-
-}
-
-func binaryToImage(data []byte) ([]byte, error) {
- reader := bytes.NewReader(data)
- img, _, err := image.Decode(reader)
- if err != nil {
- return nil, fmt.Errorf("Unable to covert bytes to image")
- }
-
- return ImageToBytes(img)
-}
-
-func min(a, b int) int {
- if a < b {
- return a
- }
- return b
-}
diff --git a/packages/tui/internal/image/clipboard_windows.go b/packages/tui/internal/image/clipboard_windows.go
deleted file mode 100644
index 6431ce3d4..000000000
--- a/packages/tui/internal/image/clipboard_windows.go
+++ /dev/null
@@ -1,192 +0,0 @@
-//go:build windows
-
-package image
-
-import (
- "bytes"
- "fmt"
- "image"
- "image/color"
- "log/slog"
- "syscall"
- "unsafe"
-)
-
-var (
- user32 = syscall.NewLazyDLL("user32.dll")
- kernel32 = syscall.NewLazyDLL("kernel32.dll")
- openClipboard = user32.NewProc("OpenClipboard")
- closeClipboard = user32.NewProc("CloseClipboard")
- getClipboardData = user32.NewProc("GetClipboardData")
- isClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable")
- globalLock = kernel32.NewProc("GlobalLock")
- globalUnlock = kernel32.NewProc("GlobalUnlock")
- globalSize = kernel32.NewProc("GlobalSize")
-)
-
-const (
- CF_TEXT = 1
- CF_UNICODETEXT = 13
- CF_DIB = 8
-)
-
-type BITMAPINFOHEADER struct {
- BiSize uint32
- BiWidth int32
- BiHeight int32
- BiPlanes uint16
- BiBitCount uint16
- BiCompression uint32
- BiSizeImage uint32
- BiXPelsPerMeter int32
- BiYPelsPerMeter int32
- BiClrUsed uint32
- BiClrImportant uint32
-}
-
-func GetImageFromClipboard() ([]byte, string, error) {
- ret, _, _ := openClipboard.Call(0)
- if ret == 0 {
- return nil, "", fmt.Errorf("failed to open clipboard")
- }
- defer func(closeClipboard *syscall.LazyProc, a ...uintptr) {
- _, _, err := closeClipboard.Call(a...)
- if err != nil {
- slog.Error("close clipboard failed")
- return
- }
- }(closeClipboard)
- isTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_TEXT))
- isUnicodeTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_UNICODETEXT))
-
- if isTextAvailable != 0 || isUnicodeTextAvailable != 0 {
- // Get text from clipboard
- var formatToUse uintptr = CF_TEXT
- if isUnicodeTextAvailable != 0 {
- formatToUse = CF_UNICODETEXT
- }
-
- hClipboardText, _, _ := getClipboardData.Call(formatToUse)
- if hClipboardText != 0 {
- textPtr, _, _ := globalLock.Call(hClipboardText)
- if textPtr != 0 {
- defer func(globalUnlock *syscall.LazyProc, a ...uintptr) {
- _, _, err := globalUnlock.Call(a...)
- if err != nil {
- slog.Error("Global unlock failed")
- return
- }
- }(globalUnlock, hClipboardText)
-
- // Get clipboard text
- var clipboardText string
- if formatToUse == CF_UNICODETEXT {
- // Convert wide string to Go string
- clipboardText = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(textPtr))[:])
- } else {
- // Get size of ANSI text
- size, _, _ := globalSize.Call(hClipboardText)
- if size > 0 {
- // Convert ANSI string to Go string
- textBytes := make([]byte, size)
- copy(textBytes, (*[1 << 20]byte)(unsafe.Pointer(textPtr))[:size:size])
- clipboardText = bytesToString(textBytes)
- }
- }
-
- // Check if the text is not empty
- if clipboardText != "" {
- return nil, clipboardText, nil
- }
- }
- }
- }
- hClipboardData, _, _ := getClipboardData.Call(uintptr(CF_DIB))
- if hClipboardData == 0 {
- return nil, "", fmt.Errorf("failed to get clipboard data")
- }
-
- dataPtr, _, _ := globalLock.Call(hClipboardData)
- if dataPtr == 0 {
- return nil, "", fmt.Errorf("failed to lock clipboard data")
- }
- defer func(globalUnlock *syscall.LazyProc, a ...uintptr) {
- _, _, err := globalUnlock.Call(a...)
- if err != nil {
- slog.Error("Global unlock failed")
- return
- }
- }(globalUnlock, hClipboardData)
-
- bmiHeader := (*BITMAPINFOHEADER)(unsafe.Pointer(dataPtr))
-
- width := int(bmiHeader.BiWidth)
- height := int(bmiHeader.BiHeight)
- if height < 0 {
- height = -height
- }
- bitsPerPixel := int(bmiHeader.BiBitCount)
-
- img := image.NewRGBA(image.Rect(0, 0, width, height))
-
- var bitsOffset uintptr
- if bitsPerPixel <= 8 {
- numColors := uint32(1) << bitsPerPixel
- if bmiHeader.BiClrUsed > 0 {
- numColors = bmiHeader.BiClrUsed
- }
- bitsOffset = unsafe.Sizeof(*bmiHeader) + uintptr(numColors*4)
- } else {
- bitsOffset = unsafe.Sizeof(*bmiHeader)
- }
-
- for y := range height {
- for x := range width {
-
- srcY := height - y - 1
- if bmiHeader.BiHeight < 0 {
- srcY = y
- }
-
- var pixelPointer unsafe.Pointer
- var r, g, b, a uint8
-
- switch bitsPerPixel {
- case 24:
- stride := (width*3 + 3) &^ 3
- pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*stride+x*3))
- b = *(*byte)(pixelPointer)
- g = *(*byte)(unsafe.Add(pixelPointer, 1))
- r = *(*byte)(unsafe.Add(pixelPointer, 2))
- a = 255
- case 32:
- pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*width*4+x*4))
- b = *(*byte)(pixelPointer)
- g = *(*byte)(unsafe.Add(pixelPointer, 1))
- r = *(*byte)(unsafe.Add(pixelPointer, 2))
- a = *(*byte)(unsafe.Add(pixelPointer, 3))
- if a == 0 {
- a = 255
- }
- default:
- return nil, "", fmt.Errorf("unsupported bit count: %d", bitsPerPixel)
- }
-
- img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a})
- }
- }
-
- imageBytes, err := ImageToBytes(img)
- if err != nil {
- return nil, "", err
- }
- return imageBytes, "", nil
-}
-
-func bytesToString(b []byte) string {
- i := bytes.IndexByte(b, 0)
- if i == -1 {
- return string(b)
- }
- return string(b[:i])
-}
diff --git a/packages/tui/internal/image/images.go b/packages/tui/internal/image/images.go
deleted file mode 100644
index 742eb30a8..000000000
--- a/packages/tui/internal/image/images.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package image
-
-import (
- "bytes"
- "fmt"
- "image"
- "image/color"
- "image/png"
- "os"
- "strings"
-
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/disintegration/imaging"
- "github.com/lucasb-eyer/go-colorful"
- _ "golang.org/x/image/webp"
-)
-
-func ValidateFileSize(filePath string, sizeLimit int64) (bool, error) {
- fileInfo, err := os.Stat(filePath)
- if err != nil {
- return false, fmt.Errorf("error getting file info: %w", err)
- }
-
- if fileInfo.Size() > sizeLimit {
- return true, nil
- }
-
- return false, nil
-}
-
-func ToString(width int, img image.Image) string {
- img = imaging.Resize(img, width, 0, imaging.Lanczos)
- b := img.Bounds()
- imageWidth := b.Max.X
- h := b.Max.Y
- str := strings.Builder{}
-
- for heightCounter := 0; heightCounter < h; heightCounter += 2 {
- for x := range imageWidth {
- c1, _ := colorful.MakeColor(img.At(x, heightCounter))
- color1 := lipgloss.Color(c1.Hex())
-
- var color2 color.Color
- if heightCounter+1 < h {
- c2, _ := colorful.MakeColor(img.At(x, heightCounter+1))
- color2 = lipgloss.Color(c2.Hex())
- } else {
- color2 = color1
- }
-
- str.WriteString(lipgloss.NewStyle().Foreground(color1).
- Background(color2).Render("▀"))
- }
-
- str.WriteString("\n")
- }
-
- return str.String()
-}
-
-func ImagePreview(width int, filename string) (string, error) {
- imageContent, err := os.Open(filename)
- if err != nil {
- return "", err
- }
- defer imageContent.Close()
-
- img, _, err := image.Decode(imageContent)
- if err != nil {
- return "", err
- }
-
- imageString := ToString(width, img)
-
- return imageString, nil
-}
-
-func ImageToBytes(image image.Image) ([]byte, error) {
- buf := new(bytes.Buffer)
- err := png.Encode(buf, image)
- if err != nil {
- return nil, err
- }
-
- return buf.Bytes(), nil
-}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index e32575bd4..d25fc1395 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -841,7 +841,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
return a, toast.NewErrorToast("Failed to share session")
}
shareUrl := response.Share.URL
- cmds = append(cmds, tea.SetClipboard(shareUrl))
+ cmds = append(cmds, a.app.SetClipboard(shareUrl))
cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!"))
case commands.SessionUnshareCommand:
if a.app.Session.ID == "" {
@@ -975,7 +975,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
case commands.MessagesCopyCommand:
selected := a.messages.Selected()
if selected != "" {
- cmd = tea.SetClipboard(selected)
+ cmd = a.app.SetClipboard(selected)
cmds = append(cmds, cmd)
cmd = toast.NewSuccessToast("Message copied to clipboard")
cmds = append(cmds, cmd)
diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml
index c8411903c..4b404aded 100644
--- a/packages/tui/sdk/.stats.yml
+++ b/packages/tui/sdk/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 20
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-4955370de3d0a21bb41c4e51257210b3284deb5bc3dbace6e7572de0d1635c9e.yml
-openapi_spec_hash: b7591d636977423cd7455aa02caa718f
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-c06a9b8d8284683e8350fdd3eceff0b5756877f7b67e974acd565409b67d32a0.yml
+openapi_spec_hash: 5933bca0c79177065374ac724a6bc986
config_hash: de53ecf98e1038f2cc2fd273b582f082
diff --git a/packages/tui/sdk/config.go b/packages/tui/sdk/config.go
index 39da2f942..503c17bd9 100644
--- a/packages/tui/sdk/config.go
+++ b/packages/tui/sdk/config.go
@@ -397,14 +397,18 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
type Keybinds struct {
// Exit the application
AppExit string `json:"app_exit"`
+ // Show help dialog
+ AppHelp string `json:"app_help"`
// Open external editor
EditorOpen string `json:"editor_open"`
- // Show help dialog
- Help string `json:"help"`
- // Navigate to next history item
- HistoryNext string `json:"history_next"`
- // Navigate to previous history item
- HistoryPrevious string `json:"history_previous"`
+ // Close file
+ FileClose string `json:"file_close"`
+ // Toggle split/unified diff
+ FileDiffToggle string `json:"file_diff_toggle"`
+ // List files
+ FileList string `json:"file_list"`
+ // Search file
+ FileSearch string `json:"file_search"`
// Clear input field
InputClear string `json:"input_clear"`
// Insert newline in input
@@ -415,6 +419,8 @@ type Keybinds struct {
InputSubmit string `json:"input_submit"`
// Leader key for keybind combinations
Leader string `json:"leader"`
+ // Copy message
+ MessagesCopy string `json:"messages_copy"`
// Navigate to first message
MessagesFirst string `json:"messages_first"`
// Scroll messages down by half page
@@ -423,6 +429,8 @@ type Keybinds struct {
MessagesHalfPageUp string `json:"messages_half_page_up"`
// Navigate to last message
MessagesLast string `json:"messages_last"`
+ // Toggle layout
+ MessagesLayoutToggle string `json:"messages_layout_toggle"`
// Navigate to next message
MessagesNext string `json:"messages_next"`
// Scroll messages down by one page
@@ -431,6 +439,8 @@ type Keybinds struct {
MessagesPageUp string `json:"messages_page_up"`
// Navigate to previous message
MessagesPrevious string `json:"messages_previous"`
+ // Revert message
+ MessagesRevert string `json:"messages_revert"`
// List available models
ModelList string `json:"model_list"`
// Initialize project configuration
@@ -445,6 +455,8 @@ type Keybinds struct {
SessionNew string `json:"session_new"`
// Share current session
SessionShare string `json:"session_share"`
+ // Unshare current session
+ SessionUnshare string `json:"session_unshare"`
// List available themes
ThemeList string `json:"theme_list"`
// Show tool details
@@ -455,23 +467,28 @@ type Keybinds struct {
// keybindsJSON contains the JSON metadata for the struct [Keybinds]
type keybindsJSON struct {
AppExit apijson.Field
+ AppHelp apijson.Field
EditorOpen apijson.Field
- Help apijson.Field
- HistoryNext apijson.Field
- HistoryPrevious apijson.Field
+ FileClose apijson.Field
+ FileDiffToggle apijson.Field
+ FileList apijson.Field
+ FileSearch apijson.Field
InputClear apijson.Field
InputNewline apijson.Field
InputPaste apijson.Field
InputSubmit apijson.Field
Leader apijson.Field
+ MessagesCopy apijson.Field
MessagesFirst apijson.Field
MessagesHalfPageDown apijson.Field
MessagesHalfPageUp apijson.Field
MessagesLast apijson.Field
+ MessagesLayoutToggle apijson.Field
MessagesNext apijson.Field
MessagesPageDown apijson.Field
MessagesPageUp apijson.Field
MessagesPrevious apijson.Field
+ MessagesRevert apijson.Field
ModelList apijson.Field
ProjectInit apijson.Field
SessionCompact apijson.Field
@@ -479,6 +496,7 @@ type keybindsJSON struct {
SessionList apijson.Field
SessionNew apijson.Field
SessionShare apijson.Field
+ SessionUnshare apijson.Field
ThemeList apijson.Field
ToolDetails apijson.Field
raw string
diff --git a/packages/tui/sdk/scripts/lint b/packages/tui/sdk/scripts/lint
index fa7ba1f6b..c10fa02f6 100755
--- a/packages/tui/sdk/scripts/lint
+++ b/packages/tui/sdk/scripts/lint
@@ -5,4 +5,7 @@ set -e
cd "$(dirname "$0")/.."
echo "==> Running Go build"
-go build ./...
+go build .
+
+# Compile the tests but don't run them
+go test -c .