fix(editor): fix opening `$EDITOR` w/ and w/o args (#1520)

Amolith created

Change summary

internal/tui/components/chat/editor/editor.go |  8 +----
internal/tui/util/shell.go                    | 26 +++++++++++++++++++++
2 files changed, 28 insertions(+), 6 deletions(-)

Detailed changes

internal/tui/components/chat/editor/editor.go 🔗

@@ -6,7 +6,6 @@ import (
 	"math/rand"
 	"net/http"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"runtime"
 	"slices"
@@ -112,11 +111,8 @@ func (m *editorCmp) openEditor(value string) tea.Cmd {
 	if _, err := tmpfile.WriteString(value); err != nil {
 		return util.ReportError(err)
 	}
-	c := exec.CommandContext(context.TODO(), editor, tmpfile.Name())
-	c.Stdin = os.Stdin
-	c.Stdout = os.Stdout
-	c.Stderr = os.Stderr
-	return tea.ExecProcess(c, func(err error) tea.Msg {
+	cmdStr := editor + " " + tmpfile.Name()
+	return util.ExecShell(context.TODO(), cmdStr, func(err error) tea.Msg {
 		if err != nil {
 			return util.ReportError(err)
 		}

internal/tui/util/shell.go 🔗

@@ -0,0 +1,26 @@
+package util
+
+import (
+	"context"
+	"errors"
+	"os/exec"
+
+	tea "charm.land/bubbletea/v2"
+	"mvdan.cc/sh/v3/shell"
+)
+
+// ExecShell parses a shell command string and executes it with exec.Command.
+// Uses shell.Fields for proper handling of shell syntax like quotes and
+// arguments while preserving TTY handling for terminal editors.
+func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
+	fields, err := shell.Fields(cmdStr, nil)
+	if err != nil {
+		return ReportError(err)
+	}
+	if len(fields) == 0 {
+		return ReportError(errors.New("empty command"))
+	}
+
+	cmd := exec.CommandContext(ctx, fields[0], fields[1:]...)
+	return tea.ExecProcess(cmd, callback)
+}