on main help sign in
Merge branch 'main' into ui
commit8ab82f5cf46d7423138498a10f68cb2df41bf1be authorAyman Bagabas <ayman.bagabas@gmail.com> dateWed 26 Nov 2025, 14:09 (1 year ago) parentece936a64637e5
5 files changed+75−34
internal/agent/coordinator.go+7−19 internal/config/config.go+1−3 internal/tui/components/chat/editor/editor.go+6−7 internal/tui/components/dialogs/models/models.go+3−5 internal/tui/util/shell.go+58
internal/agent/coordinator.go+7−19
@@ -475,27 +475,15 @@ func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error
475475}
476476
477477func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
478− hasBearerAuth := false
479− for key := range headers {
480− if strings.ToLower(key) == "authorization" {
481− hasBearerAuth = true
482− break
483− }
484− }
485−
486− isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
487−
488478 var opts []anthropic.Option
489− if apiKey != "" && !hasBearerAuth {
490− if isBearerToken {
491− slog.Debug("API key starts with 'Bearer ', using as Authorization header")
492− headers["Authorization"] = apiKey
493− apiKey = "" // clear apiKey to avoid using X-Api-Key header
494− }
495− }
496479
497− if apiKey != "" {
498− // Use standard X-Api-Key header
480+ if strings.HasPrefix(apiKey, "Bearer ") {
481+ // NOTE: Prevent the SDK from picking up the API key from env.
482+ os.Setenv("ANTHROPIC_API_KEY", "")
483+
484+ headers["Authorization"] = apiKey
485+ } else if apiKey != "" {
486+ // X-Api-Key header
499487 opts = append(opts, anthropic.WithAPIKey(apiKey))
500488 }
501489
internal/config/config.go+1−3
@@ -117,9 +117,7 @@ type ProviderConfig struct {
117117}
118118
119119func (pc *ProviderConfig) SetupClaudeCode() {
120− if !strings.HasPrefix(pc.APIKey, "Bearer ") {
121− pc.APIKey = fmt.Sprintf("Bearer %s", pc.APIKey)
122− }
120+ pc.APIKey = fmt.Sprintf("Bearer %s", pc.OAuthToken.AccessToken)
123121 pc.SystemPromptPrefix = "You are Claude Code, Anthropic's official CLI for Claude."
124122 pc.ExtraHeaders["anthropic-version"] = "2023-06-01"
125123
internal/tui/components/chat/editor/editor.go+6−7
@@ -6,7 +6,6 @@ import (
66 "math/rand"
77 "net/http"
88 "os"
9− "os/exec"
109 "path/filepath"
1110 "runtime"
1211 "slices"
@@ -96,7 +95,7 @@ type OpenEditorMsg struct {
9695func (m *editorCmp) openEditor(value string) tea.Cmd {
9796 editor := os.Getenv("EDITOR")
9897 if editor == "" {
99− // Use platform-appropriate default editor
98+ // Use platform-appropriate default editor.
10099 if runtime.GOOS == "windows" {
101100 editor = "notepad"
102101 } else {
@@ -112,11 +111,11 @@ func (m *editorCmp) openEditor(value string) tea.Cmd {
112111 if _, err := tmpfile.WriteString(value); err != nil {
113112 return util.ReportError(err)
114113 }
115− c := exec.CommandContext(context.TODO(), editor, tmpfile.Name())
116− c.Stdin = os.Stdin
117− c.Stdout = os.Stdout
118− c.Stderr = os.Stderr
119− return tea.ExecProcess(c, func(err error) tea.Msg {
114+
115+ // Build the full shell command with the file argument.
116+ cmdStr := fmt.Sprintf("%s %s", editor, tmpfile.Name())
117+
118+ return util.ExecShell(context.TODO(), cmdStr, func(err error) tea.Msg {
120119 if err != nil {
121120 return util.ReportError(err)
122121 }
internal/tui/components/dialogs/models/models.go+3−5
@@ -154,11 +154,9 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
154154 util.ReportInfo("URL copied to clipboard"),
155155 )
156156 }
157− case key.Matches(msg, m.keyMap.Choose):
158− if m.showClaudeAuthMethodChooser {
159− m.claudeAuthMethodChooser.ToggleChoice()
160− return m, nil
161− }
157+ case key.Matches(msg, m.keyMap.Choose) && m.showClaudeAuthMethodChooser:
158+ m.claudeAuthMethodChooser.ToggleChoice()
159+ return m, nil
162160 case key.Matches(msg, m.keyMap.Select):
163161 selectedItem := m.modelList.SelectedModel()
164162
internal/tui/util/shell.gonew file+58
@@ -0,0 +1,58 @@
1+package util
2+
3+import (
4+ "context"
5+ "io"
6+ "strings"
7+
8+ tea "charm.land/bubbletea/v2"
9+ "mvdan.cc/sh/v3/interp"
10+ "mvdan.cc/sh/v3/syntax"
11+)
12+
13+// shellCommand wraps a shell interpreter to implement tea.ExecCommand.
14+type shellCommand struct {
15+ ctx context.Context
16+ file *syntax.File
17+ stdin io.Reader
18+ stdout io.Writer
19+ stderr io.Writer
20+}
21+
22+func (s *shellCommand) SetStdin(r io.Reader) {
23+ s.stdin = r
24+}
25+
26+func (s *shellCommand) SetStdout(w io.Writer) {
27+ s.stdout = w
28+}
29+
30+func (s *shellCommand) SetStderr(w io.Writer) {
31+ s.stderr = w
32+}
33+
34+func (s *shellCommand) Run() error {
35+ runner, err := interp.New(
36+ interp.StdIO(s.stdin, s.stdout, s.stderr),
37+ )
38+ if err != nil {
39+ return err
40+ }
41+ return runner.Run(s.ctx, s.file)
42+}
43+
44+// ExecShell executes a shell command string using tea.Exec.
45+// The command is parsed and executed via mvdan.cc/sh/v3/interp, allowing
46+// proper handling of shell syntax like quotes and arguments.
47+func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
48+ parsed, err := syntax.NewParser().Parse(strings.NewReader(cmdStr), "")
49+ if err != nil {
50+ return ReportError(err)
51+ }
52+
53+ cmd := &shellCommand{
54+ ctx: ctx,
55+ file: parsed,
56+ }
57+ return tea.Exec(cmd, callback)
58+}