Merge branch 'main' into ui

Ayman Bagabas created

Change summary

internal/agent/coordinator.go                    | 26 ++-----
internal/config/config.go                        |  4 
internal/tui/components/chat/editor/editor.go    | 13 +--
internal/tui/components/dialogs/models/models.go |  8 -
internal/tui/util/shell.go                       | 58 ++++++++++++++++++
5 files changed, 75 insertions(+), 34 deletions(-)

Detailed changes

internal/agent/coordinator.go 🔗

@@ -475,27 +475,15 @@ func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error
 }
 
 func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
-	hasBearerAuth := false
-	for key := range headers {
-		if strings.ToLower(key) == "authorization" {
-			hasBearerAuth = true
-			break
-		}
-	}
-
-	isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
-
 	var opts []anthropic.Option
-	if apiKey != "" && !hasBearerAuth {
-		if isBearerToken {
-			slog.Debug("API key starts with 'Bearer ', using as Authorization header")
-			headers["Authorization"] = apiKey
-			apiKey = "" // clear apiKey to avoid using X-Api-Key header
-		}
-	}
 
-	if apiKey != "" {
-		// Use standard X-Api-Key header
+	if strings.HasPrefix(apiKey, "Bearer ") {
+		// NOTE: Prevent the SDK from picking up the API key from env.
+		os.Setenv("ANTHROPIC_API_KEY", "")
+
+		headers["Authorization"] = apiKey
+	} else if apiKey != "" {
+		// X-Api-Key header
 		opts = append(opts, anthropic.WithAPIKey(apiKey))
 	}
 

internal/config/config.go 🔗

@@ -117,9 +117,7 @@ type ProviderConfig struct {
 }
 
 func (pc *ProviderConfig) SetupClaudeCode() {
-	if !strings.HasPrefix(pc.APIKey, "Bearer ") {
-		pc.APIKey = fmt.Sprintf("Bearer %s", pc.APIKey)
-	}
+	pc.APIKey = fmt.Sprintf("Bearer %s", pc.OAuthToken.AccessToken)
 	pc.SystemPromptPrefix = "You are Claude Code, Anthropic's official CLI for Claude."
 	pc.ExtraHeaders["anthropic-version"] = "2023-06-01"
 

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

@@ -6,7 +6,6 @@ import (
 	"math/rand"
 	"net/http"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"runtime"
 	"slices"
@@ -96,7 +95,7 @@ type OpenEditorMsg struct {
 func (m *editorCmp) openEditor(value string) tea.Cmd {
 	editor := os.Getenv("EDITOR")
 	if editor == "" {
-		// Use platform-appropriate default editor
+		// Use platform-appropriate default editor.
 		if runtime.GOOS == "windows" {
 			editor = "notepad"
 		} else {
@@ -112,11 +111,11 @@ 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 {
+
+	// Build the full shell command with the file argument.
+	cmdStr := fmt.Sprintf("%s %s", editor, tmpfile.Name())
+
+	return util.ExecShell(context.TODO(), cmdStr, func(err error) tea.Msg {
 		if err != nil {
 			return util.ReportError(err)
 		}

internal/tui/components/dialogs/models/models.go 🔗

@@ -154,11 +154,9 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 					util.ReportInfo("URL copied to clipboard"),
 				)
 			}
-		case key.Matches(msg, m.keyMap.Choose):
-			if m.showClaudeAuthMethodChooser {
-				m.claudeAuthMethodChooser.ToggleChoice()
-				return m, nil
-			}
+		case key.Matches(msg, m.keyMap.Choose) && m.showClaudeAuthMethodChooser:
+			m.claudeAuthMethodChooser.ToggleChoice()
+			return m, nil
 		case key.Matches(msg, m.keyMap.Select):
 			selectedItem := m.modelList.SelectedModel()
 

internal/tui/util/shell.go 🔗

@@ -0,0 +1,58 @@
+package util
+
+import (
+	"context"
+	"io"
+	"strings"
+
+	tea "charm.land/bubbletea/v2"
+	"mvdan.cc/sh/v3/interp"
+	"mvdan.cc/sh/v3/syntax"
+)
+
+// shellCommand wraps a shell interpreter to implement tea.ExecCommand.
+type shellCommand struct {
+	ctx    context.Context
+	file   *syntax.File
+	stdin  io.Reader
+	stdout io.Writer
+	stderr io.Writer
+}
+
+func (s *shellCommand) SetStdin(r io.Reader) {
+	s.stdin = r
+}
+
+func (s *shellCommand) SetStdout(w io.Writer) {
+	s.stdout = w
+}
+
+func (s *shellCommand) SetStderr(w io.Writer) {
+	s.stderr = w
+}
+
+func (s *shellCommand) Run() error {
+	runner, err := interp.New(
+		interp.StdIO(s.stdin, s.stdout, s.stderr),
+	)
+	if err != nil {
+		return err
+	}
+	return runner.Run(s.ctx, s.file)
+}
+
+// ExecShell executes a shell command string using tea.Exec.
+// The command is parsed and executed via mvdan.cc/sh/v3/interp, allowing
+// proper handling of shell syntax like quotes and arguments.
+func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
+	parsed, err := syntax.NewParser().Parse(strings.NewReader(cmdStr), "")
+	if err != nil {
+		return ReportError(err)
+	}
+
+	cmd := &shellCommand{
+		ctx:  ctx,
+		file: parsed,
+	}
+	return tea.Exec(cmd, callback)
+}