on main help sign in
fix: fix pasting files on some terminal emulators (#2106)

* Check `WT_SESSION` instead of `GOOS` for Windows Terminal. * Be more strict on Windows Terminal: do not allow chars outside quotes (prevents false positives). * Some terminals just paste the literal paths (Rio as separate events, Kitty separated by a line break). If it contains valid path(s) for existing file(s), just use that. * Workaround Rio on Windows that adds NULL chars to the string.

commit3c8be6926cda50f4129e358bf78af65e7b315d32 authorAndrey Nering <andreynering@users.noreply.github.com> dateTue 3 Feb 2026, 16:06 (8 months ago) parent02ec682
3 files changed+37−16
internal/fsext/paste.go+27−9 internal/fsext/paste_test.go+6−6 internal/ui/model/ui.go+4−1
internal/fsext/paste.go+27−9
@@ -1,20 +1,36 @@
11package fsext
22
33import (
4− "runtime"
4+ "os"
55 "strings"
66)
77
8−func PasteStringToPaths(s string) []string {
9− switch runtime.GOOS {
10− case "windows":
11− return windowsPasteStringToPaths(s)
8+func ParsePastedFiles(s string) []string {
9+ s = strings.TrimSpace(s)
10+
11+ // NOTE: Rio on Windows adds NULL chars for some reason.
12+ s = strings.ReplaceAll(s, "\x00", "")
13+
14+ switch {
15+ case attemptStat(s):
16+ return strings.Split(s, "\n")
17+ case os.Getenv("WT_SESSION") != "":
18+ return windowsTerminalParsePastedFiles(s)
1219 default:
13− return unixPasteStringToPaths(s)
20+ return unixParsePastedFiles(s)
21+ }
22+}
23+
24+func attemptStat(s string) bool {
25+ for path := range strings.SplitSeq(s, "\n") {
26+ if info, err := os.Stat(path); err != nil || info.IsDir() {
27+ return false
28+ }
1429 }
30+ return true
1531}
1632
17−func windowsPasteStringToPaths(s string) []string {
33+func windowsTerminalParsePastedFiles(s string) []string {
1834 if strings.TrimSpace(s) == "" {
1935 return nil
2036 }
@@ -42,8 +58,10 @@ func windowsPasteStringToPaths(s string) []string {
4258 }
4359 case inQuotes:
4460 current.WriteByte(ch)
61+ case ch != ' ':
62+ // Text outside quotes is not allowed
63+ return nil
4564 }
46− // Skip characters outside quotes and spaces between quoted sections
4765 }
4866
4967 // Add any remaining content if quotes were properly closed
@@ -59,7 +77,7 @@ func windowsPasteStringToPaths(s string) []string {
5977 return paths
6078}
6179
62−func unixPasteStringToPaths(s string) []string {
80+func unixParsePastedFiles(s string) []string {
6381 if strings.TrimSpace(s) == "" {
6482 return nil
6583 }
internal/fsext/paste_test.go+6−6
@@ -6,8 +6,8 @@ import (
66 "github.com/stretchr/testify/require"
77)
88
9−func TestPasteStringToPaths(t *testing.T) {
10− t.Run("Windows", func(t *testing.T) {
9+func TestParsePastedFiles(t *testing.T) {
10+ t.Run("WindowsTerminal", func(t *testing.T) {
1111 tests := []struct {
1212 name string
1313 input string
@@ -24,7 +24,7 @@ func TestPasteStringToPaths(t *testing.T) {
2424 expected: []string{`C:\path\my-screenshot-one.png`, `C:\path\my-screenshot-two.png`, `C:\path\my-screenshot-three.png`},
2525 },
2626 {
27− name: "sigle with spaces",
27+ name: "single with spaces",
2828 input: `"C:\path\my screenshot one.png"`,
2929 expected: []string{`C:\path\my screenshot one.png`},
3030 },
@@ -46,7 +46,7 @@ func TestPasteStringToPaths(t *testing.T) {
4646 {
4747 name: "text outside quotes",
4848 input: `"C:\path\file.png" some random text "C:\path\file2.png"`,
49− expected: []string{`C:\path\file.png`, `C:\path\file2.png`},
49+ expected: nil,
5050 },
5151 {
5252 name: "multiple spaces between paths",
@@ -66,7 +66,7 @@ func TestPasteStringToPaths(t *testing.T) {
6666 }
6767 for _, tt := range tests {
6868 t.Run(tt.name, func(t *testing.T) {
69− result := windowsPasteStringToPaths(tt.input)
69+ result := windowsTerminalParsePastedFiles(tt.input)
7070 require.Equal(t, tt.expected, result)
7171 })
7272 }
@@ -141,7 +141,7 @@ func TestPasteStringToPaths(t *testing.T) {
141141 }
142142 for _, tt := range tests {
143143 t.Run(tt.name, func(t *testing.T) {
144− result := unixPasteStringToPaths(tt.input)
144+ result := unixParsePastedFiles(tt.input)
145145 require.Equal(t, tt.expected, result)
146146 })
147147 }
internal/ui/model/ui.go+4−1
@@ -2915,7 +2915,7 @@ func (m *UI) handlePasteMsg(msg tea.PasteMsg) tea.Cmd {
29152915 // Attempt to parse pasted content as file paths. If possible to parse,
29162916 // all files exist and are valid, add as attachments.
29172917 // Otherwise, paste as text.
2918− paths := fsext.PasteStringToPaths(msg.Content)
2918+ paths := fsext.ParsePastedFiles(msg.Content)
29192919 allExistsAndValid := func() bool {
29202920 for _, path := range paths {
29212921 if _, err := os.Stat(path); os.IsNotExist(err) {
@@ -2956,6 +2956,9 @@ func (m *UI) handleFilePathPaste(path string) tea.Cmd {
29562956 if err != nil {
29572957 return uiutil.ReportError(err)
29582958 }
2959+ if fileInfo.IsDir() {
2960+ return uiutil.ReportWarn("Cannot attach a directory")
2961+ }
29592962 if fileInfo.Size() > common.MaxAttachmentSize {
29602963 return uiutil.ReportWarn("File is too big (>5mb)")
29612964 }