返回 DeepSeek-Reasonix
connection_setup.go
根目录 / internal / cli / connection_setup.go
1 package cli
2
3 import (
4 "context"
5 "crypto/rand"
6 "encoding/hex"
7 "fmt"
8 "strings"
9 "unicode/utf8"
10
11 tea "charm.land/bubbletea/v2"
12 "github.com/charmbracelet/x/ansi"
13
14 "reasonix/internal/boot"
15 "reasonix/internal/config"
16 )
17
18 type connectionSetup struct {
19 providerName string
20 configPath string
21 revision string
22 key string
23 saving bool
24 testing bool
25 testCancel context.CancelFunc
26 testVersion uint64
27 }
28
29 type connectionCredentialTestedMsg struct {
30 providerName string
31 setup *connectionSetup
32 version uint64
33 err error
34 }
35
36 type connectionCredentialSavedMsg struct {
37 providerName string
38 result config.ConnectionCredentialResult
39 err error
40 }
41
42 func (m *chatTUI) openConnectionSetup() {
43 cfg, err := config.LoadForRootReadOnly(m.ctrl.WorkspaceRoot())
44 if err != nil {
45 m.notice("setup: " + err.Error())
46 return
47 }
48 current := strings.SplitN(m.modelRef, "/", 2)[0]
49 items := make([]quickPickerItem, 0, len(cfg.Providers))
50 selected := 0
51 for _, entry := range cfg.Providers {
52 if strings.TrimSpace(entry.Name) == "" || len(entry.ModelList()) == 0 {
53 continue
54 }
55 status := ""
56 if entry.Name == current {
57 status = "active"
58 selected = len(items)
59 }
60 description := fmt.Sprintf("%s · %d model(s)", entry.Kind, len(entry.ModelList()))
61 if entry.RequiresAPIKey() && entry.APIKey() == "" {
62 description += " · key required"
63 }
64 items = append(items, quickPickerItem{ID: entry.Name, Label: entry.Name, Description: description, Status: status})
65 }
66 if len(items) == 0 {
67 m.notice("setup: no model connections are available")
68 return
69 }
70 m.quickPick = &quickPicker{kind: quickPickerSetupProvider, title: "Configure connection", items: items, selected: selected}
71 }
72
73 func (m *chatTUI) beginConnectionKeyEdit(providerName string) {
74 root := strings.TrimSpace(m.ctrl.WorkspaceRoot())
75 // Resolve the same source precedence as runtime loading: a user provider
76 // shadows a same-named project declaration.
77 effective, err := config.LoadForRootReadOnly(root)
78 if err != nil {
79 m.notice("setup: " + err.Error())
80 return
81 }
82 path, err := effective.ProviderEditPath(root, providerName)
83 if err != nil {
84 m.notice("setup: " + err.Error())
85 return
86 }
87 m.setup = &connectionSetup{providerName: providerName, configPath: path, revision: config.ConfigFileRevision(path)}
88 }
89
90 func (m chatTUI) handleConnectionSetupKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
91 setup := m.setup
92 if setup == nil || setup.saving {
93 return m, nil
94 }
95 switch msg.String() {
96 case "esc":
97 if setup.testCancel != nil {
98 setup.testCancel()
99 }
100 m.setup = nil
101 return m, nil
102 case "ctrl+t":
103 if setup.testing {
104 return m, nil
105 }
106 if setup.key == "" {
107 m.notice("setup: enter an API key before testing")
108 return m, nil
109 }
110 cfg, err := config.LoadForEditReadOnlyStrict(setup.configPath)
111 if err != nil {
112 m.notice("setup: " + err.Error())
113 return m, nil
114 }
115 entry, ok := cfg.Provider(setup.providerName)
116 if !ok {
117 m.notice("setup: connection no longer exists")
118 return m, nil
119 }
120 ctx, cancel := context.WithCancel(context.Background())
121 setup.testVersion++
122 version := setup.testVersion
123 setup.testing, setup.testCancel = true, cancel
124 probeEntry := *entry
125 key := setup.key
126 proxy := cfg.NetworkProxySpec()
127 return m, func() tea.Msg {
128 err := boot.ProbeProviderConnection(ctx, probeEntry, key, proxy)
129 return connectionCredentialTestedMsg{providerName: setup.providerName, setup: setup, version: version, err: err}
130 }
131 case "backspace":
132 if setup.key != "" {
133 setup.invalidateTest()
134 _, n := utf8.DecodeLastRuneInString(setup.key)
135 setup.key = setup.key[:len(setup.key)-n]
136 }
137 return m, nil
138 case "enter":
139 if setup.key == "" {
140 m.notice("setup: enter an API key")
141 return m, nil
142 }
143 setup.invalidateTest()
144 requestID, err := newConnectionSetupRequestID()
145 if err != nil {
146 m.notice("setup: " + err.Error())
147 return m, nil
148 }
149 setup.saving = true
150 req := config.ConnectionCredentialRequest{
151 RequestID: requestID, ConfigPath: setup.configPath,
152 ProviderNames: []string{setup.providerName}, Key: setup.key, ExpectedRevision: setup.revision,
153 }
154 return m, func() tea.Msg {
155 result, err := config.CommitConnectionCredential(req)
156 return connectionCredentialSavedMsg{providerName: setup.providerName, result: result, err: err}
157 }
158 default:
159 text := msg.Text
160 if text == "" {
161 s := msg.String()
162 if len(s) == 1 && s[0] >= 32 && s[0] < 127 {
163 text = s
164 }
165 }
166 if text != "" && !strings.ContainsAny(text, "\r\n") {
167 setup.invalidateTest()
168 setup.key += text
169 }
170 return m, nil
171 }
172 }
173
174 func (m chatTUI) renderConnectionSetup() string {
175 if m.setup == nil {
176 return ""
177 }
178 w := max(m.width, 10)
179 contentWidth := max(w-8, 12)
180 var body strings.Builder
181 body.WriteString(accent("Configure "+m.setup.providerName) + "\n")
182 body.WriteString(" API Key\n")
183 masked := strings.Repeat("•", utf8.RuneCountInString(m.setup.key))
184 if masked == "" {
185 masked = dim("Enter credential")
186 }
187 body.WriteString(" " + ansi.Truncate(masked, contentWidth, "…") + "\n")
188 if m.setup.saving {
189 body.WriteString(dim("Saving…"))
190 } else if m.setup.testing {
191 body.WriteString(dim("Testing connection… · Esc cancel"))
192 } else {
193 body.WriteString(dim("Ctrl+T test · Enter save · Esc cancel"))
194 }
195 return choicePanelStyle.Width(w).Render(body.String())
196 }
197
198 func (m *chatTUI) handleConnectionCredentialTested(msg connectionCredentialTestedMsg) {
199 if m.setup == nil || m.setup != msg.setup || m.setup.providerName != msg.providerName || m.setup.testVersion != msg.version {
200 return
201 }
202 m.setup.testing = false
203 m.setup.testCancel = nil
204 if msg.err != nil {
205 m.notice("connection test: " + msg.err.Error())
206 return
207 }
208 m.notice("Connection test succeeded. The credential has not been saved yet.")
209 }
210
211 func (s *connectionSetup) invalidateTest() {
212 if s == nil {
213 return
214 }
215 if s.testCancel != nil {
216 s.testCancel()
217 }
218 s.testing = false
219 s.testCancel = nil
220 s.testVersion++
221 }
222
223 func newConnectionSetupRequestID() (string, error) {
224 var raw [16]byte
225 if _, err := rand.Read(raw[:]); err != nil {
226 return "", fmt.Errorf("create save request: %w", err)
227 }
228 return "cli-" + hex.EncodeToString(raw[:]), nil
229 }
230
231 func (m *chatTUI) handleConnectionCredentialSaved(msg connectionCredentialSavedMsg) tea.Cmd {
232 if msg.err != nil {
233 if m.setup != nil {
234 m.setup.saving = false
235 }
236 m.notice("setup: " + msg.err.Error())
237 return nil
238 }
239 m.setup = nil
240 m.notice("Credential saved for " + msg.providerName + ". Applying it to this session…")
241 return m.scheduleCurrentControllerRebuild("setup", "Credential saved and applied")
242 }
243
243 lines GO