on main help sign in
Merge branch 'main' into ui
commit38605b2cd928cc64525aedf5ef61fbb1cef85333 authorAyman Bagabas <ayman.bagabas@gmail.com> dateWed 19 Nov 2025, 17:08 (1 year ago) parentefaa956281e7fa
17 files changed+251−26
.github/cla-signatures.json+8 README.md+5−5 go.mod+4−3 go.sum+8−6 internal/agent/agent.go+1−1 internal/app/app.go+21 internal/config/config.go+5−1 internal/message/content.go+4−1 internal/pubsub/events.go+7 internal/shell/background_test.go+1 internal/tui/components/core/status/status.go+2−2 internal/tui/tui.go+14 internal/tui/util/util.go+1 internal/update/update.go+118 internal/update/update_test.go+48 internal/version/version.go+3−7 schema.json+1
.github/cla-signatures.json+8
@@ -863,6 +863,14 @@
863863 "created_at": "2025-11-16T20:55:47Z",
864864 "repoId": 987670088,
865865 "pullRequestNo": 1457
866+ },
867+ {
868+ "name": "micahwalter",
869+ "id": 47419,
870+ "comment_id": 3553482361,
871+ "created_at": "2025-11-19T15:59:07Z",
872+ "repoId": 987670088,
873+ "pullRequestNo": 1478
866874 }
867875 ]
868876}
README.md+5−5
@@ -186,11 +186,11 @@ That said, you can also set environment variables for preferred providers.
186186| `VERTEXAI_PROJECT` | Google Cloud VertexAI (Gemini) |
187187| `VERTEXAI_LOCATION` | Google Cloud VertexAI (Gemini) |
188188| `GROQ_API_KEY` | Groq |
189−| `AWS_ACCESS_KEY_ID` | AWS Bedrock (Claude) |
190−| `AWS_SECRET_ACCESS_KEY` | AWS Bedrock (Claude) |
191−| `AWS_REGION` | AWS Bedrock (Claude) |
192−| `AWS_PROFILE` | AWS Bedrock (Custom Profile) |
193−| `AWS_BEARER_TOKEN_BEDROCK` | AWS Bedrock |
189+| `AWS_ACCESS_KEY_ID` | Amazon Bedrock (Claude) |
190+| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock (Claude) |
191+| `AWS_REGION` | Amazon Bedrock (Claude) |
192+| `AWS_PROFILE` | Amazon Bedrock (Custom Profile) |
193+| `AWS_BEARER_TOKEN_BEDROCK` | Amazon Bedrock |
194194| `AZURE_OPENAI_API_ENDPOINT` | Azure OpenAI models |
195195| `AZURE_OPENAI_API_KEY` | Azure OpenAI models (optional when using Entra ID) |
196196| `AZURE_OPENAI_API_VERSION` | Azure OpenAI models |
go.mod+4−3
@@ -5,7 +5,7 @@ go 1.25.0
55require (
66 charm.land/bubbles/v2 v2.0.0-rc.1
77 charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251117161017-15f884bd2973
8− charm.land/fantasy v0.3.1
8+ charm.land/fantasy v0.3.2
99 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410
1010 charm.land/x/vcr v0.1.1
1111 github.com/JohannesKaufmann/html-to-markdown v1.6.0
@@ -26,7 +26,7 @@ require (
2626 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
2727 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
2828 github.com/charmbracelet/x/exp/ordered v0.1.0
29− github.com/charmbracelet/x/exp/slice v0.0.0-20251113172435-cef867b85f6a
29+ github.com/charmbracelet/x/exp/slice v0.0.0-20251118172736-77d017256798
3030 github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4
3131 github.com/charmbracelet/x/term v0.2.2
3232 github.com/denisbrodbeck/machineid v1.0.1
@@ -116,7 +116,8 @@ require (
116116 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
117117 github.com/inconshreveable/mousetrap v1.1.0 // indirect
118118 github.com/kaptinlin/go-i18n v0.2.0 // indirect
119− github.com/kaptinlin/jsonschema v0.5.2 // indirect
119+ github.com/kaptinlin/jsonpointer v0.4.6 // indirect
120+ github.com/kaptinlin/jsonschema v0.6.1 // indirect
120121 github.com/kaptinlin/messageformat-go v0.4.6 // indirect
121122 github.com/klauspost/compress v1.18.0 // indirect
122123 github.com/klauspost/cpuid/v2 v2.0.9 // indirect
go.sum+8−6
@@ -2,8 +2,8 @@ charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM
22charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
33charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251117161017-15f884bd2973 h1:Ay8VWyn/CbwltswomzWXj0m5KKfSJavFfCDCxI+j8qo=
44charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251117161017-15f884bd2973/go.mod h1:IXFmnCnMLTWw/KQ9rEatSYqbAPAYi8kA3Yqwa1SFnLk=
5−charm.land/fantasy v0.3.1 h1:YeMoLnaOHM3hdXq+SByxIKZxdm/2CHgKIS7HE0k/G6I=
6−charm.land/fantasy v0.3.1/go.mod h1:sV8Ns/JTJHOaYOHPgVRDugMheAyxsW/nmdpVGrycYEk=
5+charm.land/fantasy v0.3.2 h1:yHTsSZ25LcICMRw3xzdz3OkaZtDQch+B5ljJo17HxgU=
6+charm.land/fantasy v0.3.2/go.mod h1:sV8Ns/JTJHOaYOHPgVRDugMheAyxsW/nmdpVGrycYEk=
77charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
88charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
99charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q=
@@ -108,8 +108,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6g
108108github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
109109github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
110110github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
111−github.com/charmbracelet/x/exp/slice v0.0.0-20251113172435-cef867b85f6a h1:+mXWbAiS5wNq8VvUd+/P4STqdu2dLtCe9sFr9IqdPDk=
112−github.com/charmbracelet/x/exp/slice v0.0.0-20251113172435-cef867b85f6a/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
111+github.com/charmbracelet/x/exp/slice v0.0.0-20251118172736-77d017256798 h1:EkOQR1G3MhyPxA39njT7E33V1Y/bDbF1XxEcMmM6Ox8=
112+github.com/charmbracelet/x/exp/slice v0.0.0-20251118172736-77d017256798/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
113113github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ=
114114github.com/charmbracelet/x/json v0.2.0/go.mod h1:opFIflx2YgXgi49xVUu8gEQ21teFAxyMwvOiZhIvWNM=
115115github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4 h1:i/XilBPYK4L1Yo/mc9FPx0SyJzIsN0y4sj1MWq9Sscc=
@@ -197,8 +197,10 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
197197github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
198198github.com/kaptinlin/go-i18n v0.2.0 h1:8iwjAERQbCVF78c3HxC4MxUDxDRFvQVQlMDvlsO43hU=
199199github.com/kaptinlin/go-i18n v0.2.0/go.mod h1:gRHEMrTHtQLsAFwulPbJG71TwHjXxkagn88O8FI8FuA=
200−github.com/kaptinlin/jsonschema v0.5.2 h1:ipUBEv1/RnT+ErwdqXZ3Xtwkwp6uqp/Q9lFILrwhUfc=
201−github.com/kaptinlin/jsonschema v0.5.2/go.mod h1:HuWb90460GwFxRe0i9Ni3Z7YXwkjpqjeccWTB9gTZZE=
200+github.com/kaptinlin/jsonpointer v0.4.6 h1:hAett1YROLwxAOKZS08hsJueXr1w0fTMSvWq2x1IoUA=
201+github.com/kaptinlin/jsonpointer v0.4.6/go.mod h1:5pHXLIYd2FgV0rUEsChp6xTOvcC2OFk7kF/cjhHzL4g=
202+github.com/kaptinlin/jsonschema v0.6.1 h1:RNUQ11ZCHTtM80YcVwRm033H5OJS+MpO06d9x7Yk25o=
203+github.com/kaptinlin/jsonschema v0.6.1/go.mod h1:T8SNWNTRLDS1w+ogMZpGYqIfUXn/8DK9r06mf8XbNLE=
202204github.com/kaptinlin/messageformat-go v0.4.6 h1:57DUC9en40mGZR7MvqOS+5EYogAl465fjo+loAA1KPg=
203205github.com/kaptinlin/messageformat-go v0.4.6/go.mod h1:r0PH7FsxJX8jS/n6LAYZon5w3X+yfCLUrquqYd2H7ks=
204206github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
internal/agent/agent.go+1−1
@@ -273,7 +273,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
273273 }
274274 if googleData, ok := reasoning.ProviderMetadata[google.Name]; ok {
275275 if reasoning, ok := googleData.(*google.ReasoningMetadata); ok {
276− currentAssistant.AppendReasoningSignature(reasoning.Signature)
276+ currentAssistant.AppendThoughtSignature(reasoning.Signature, reasoning.ToolID)
277277 }
278278 }
279279 if openaiData, ok := reasoning.ProviderMetadata[openai.Name]; ok {
internal/app/app.go+21
@@ -33,6 +33,8 @@ import (
3333 "github.com/charmbracelet/crush/internal/term"
3434 "github.com/charmbracelet/crush/internal/tui/components/anim"
3535 "github.com/charmbracelet/crush/internal/tui/styles"
36+ "github.com/charmbracelet/crush/internal/update"
37+ "github.com/charmbracelet/crush/internal/version"
3638 "github.com/charmbracelet/x/ansi"
3739 "github.com/charmbracelet/x/exp/charmtone"
3840)
@@ -92,6 +94,9 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
9294 // Initialize LSP clients in the background.
9395 app.initLSPClients(ctx)
9496
97+ // Check for updates in the background.
98+ go app.checkForUpdates(ctx)
99+
95100 go func() {
96101 slog.Info("Initializing MCP clients")
97102 mcp.Initialize(ctx, app.Permissions, cfg)
@@ -390,3 +395,19 @@ func (app *App) Shutdown() {
390395 }
391396 }
392397}
398+
399+// checkForUpdates checks for available updates.
400+func (app *App) checkForUpdates(ctx context.Context) {
401+ checkCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
402+ defer cancel()
403+
404+ info, err := update.Check(checkCtx, version.Version, update.Default)
405+ if err != nil || !info.Available() {
406+ return
407+ }
408+ app.events <- pubsub.UpdateAvailableMsg{
409+ CurrentVersion: info.Current,
410+ LatestVersion: info.Latest,
411+ IsDevelopment: info.IsDevelopment(),
412+ }
413+}
internal/config/config.go+5−1
@@ -89,7 +89,7 @@ type ProviderConfig struct {
8989 // The provider's API endpoint.
9090 BaseURL string `json:"base_url,omitempty" jsonschema:"description=Base URL for the provider's API,format=uri,example=https://api.openai.com/v1"`
9191 // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
92− Type catwalk.Type `json:"type,omitempty" jsonschema:"description=Provider type that determines the API format,enum=openai,enum=anthropic,enum=gemini,enum=azure,enum=vertexai,default=openai"`
92+ Type catwalk.Type `json:"type,omitempty" jsonschema:"description=Provider type that determines the API format,enum=openai,enum=openai-compat,enum=anthropic,enum=gemini,enum=azure,enum=vertexai,default=openai"`
9393 // The provider's API key.
9494 APIKey string `json:"api_key,omitempty" jsonschema:"description=API key for authentication with the provider,example=$OPENAI_API_KEY"`
9595 // Marks the provider as disabled.
@@ -635,6 +635,10 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
635635 baseURL = "https://api.anthropic.com/v1"
636636 }
637637 testURL = baseURL + "/models"
638+ // TODO: replace with const when catwalk is released
639+ if c.ID == "kimi-coding" {
640+ testURL = baseURL + "/v1/models"
641+ }
638642 headers["x-api-key"] = apiKey
639643 headers["anthropic-version"] = "2023-06-01"
640644 case catwalk.TypeGoogle:
internal/message/content.go+4−1
@@ -45,6 +45,7 @@ type ReasoningContent struct {
4545 Thinking string `json:"thinking"`
4646 Signature string `json:"signature"`
4747 ThoughtSignature string `json:"thought_signature"` // Used for google
48+ ToolID string `json:"tool_id"` // Used for openrouter google models
4849 ResponsesData *openai.ResponsesReasoningMetadata `json:"responses_data"`
4950 StartedAt int64 `json:"started_at,omitempty"`
5051 FinishedAt int64 `json:"finished_at,omitempty"`
@@ -261,12 +262,13 @@ func (m *Message) AppendReasoningContent(delta string) {
261262 }
262263}
263264
264−func (m *Message) AppendThoughtSignature(signature string) {
265+func (m *Message) AppendThoughtSignature(signature string, toolCallID string) {
265266 for i, part := range m.Parts {
266267 if c, ok := part.(ReasoningContent); ok {
267268 m.Parts[i] = ReasoningContent{
268269 Thinking: c.Thinking,
269270 ThoughtSignature: c.ThoughtSignature + signature,
271+ ToolID: toolCallID,
270272 Signature: c.Signature,
271273 StartedAt: c.StartedAt,
272274 FinishedAt: c.FinishedAt,
@@ -464,6 +466,7 @@ func (m *Message) ToAIMessage() []fantasy.Message {
464466 if reasoning.ThoughtSignature != "" {
465467 reasoningPart.ProviderOptions[google.Name] = &google.ReasoningMetadata{
466468 Signature: reasoning.ThoughtSignature,
469+ ToolID: reasoning.ToolID,
467470 }
468471 }
469472 parts = append(parts, reasoningPart)
internal/pubsub/events.go+7
@@ -26,3 +26,10 @@ type (
2626 Publish(EventType, T)
2727 }
2828)
29+
30+// UpdateAvailableMsg is sent when a new version is available.
31+type UpdateAvailableMsg struct {
32+ CurrentVersion string
33+ LatestVersion string
34+ IsDevelopment bool
35+}
internal/shell/background_test.go+1
@@ -8,6 +8,7 @@ import (
88)
99
1010func TestBackgroundShellManager_Start(t *testing.T) {
11+ t.Skip("Skipping this until I figure out why its flaky")
1112 t.Parallel()
1213
1314 ctx := context.Background()
internal/tui/components/core/status/status.go+2−2
@@ -82,10 +82,10 @@ func (m *statusCmp) infoMsg() string {
8282 info := ansi.Truncate(m.info.Msg, widthLeft, "…")
8383 message = t.S().Base.Foreground(t.BgOverlay).Width(widthLeft+2).Background(t.Warning).Padding(0, 1).Render(info)
8484 default:
85− infoType = t.S().Base.Foreground(t.BgOverlay).Background(t.Green).Padding(0, 1).Render("OKAY!")
85+ infoType = t.S().Base.Foreground(t.BgSubtle).Background(t.Green).Padding(0, 1).Bold(true).Render("HEY!")
8686 widthLeft := m.width - (lipgloss.Width(infoType) + 2)
8787 info := ansi.Truncate(m.info.Msg, widthLeft, "…")
88− message = t.S().Base.Background(t.Success).Width(widthLeft+2).Foreground(t.White).Padding(0, 1).Render(info)
88+ message = t.S().Base.Background(t.GreenDark).Width(widthLeft+2).Foreground(t.BgSubtle).Padding(0, 1).Render(info)
8989 }
9090 return ansi.Truncate(infoType+message, m.width, "…")
9191}
internal/tui/tui.go+14
@@ -372,6 +372,20 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
372372 cmds = append(cmds, pageCmd)
373373 }
374374 return a, tea.Batch(cmds...)
375+ // Update Available
376+ case pubsub.UpdateAvailableMsg:
377+ // Show update notification in status bar
378+ statusMsg := fmt.Sprintf("Crush update available: v%s → v%s.", msg.CurrentVersion, msg.LatestVersion)
379+ if msg.IsDevelopment {
380+ statusMsg = fmt.Sprintf("This is a development version of Crush. The latest version is v%s.", msg.LatestVersion)
381+ }
382+ s, statusCmd := a.status.Update(util.InfoMsg{
383+ Type: util.InfoTypeInfo,
384+ Msg: statusMsg,
385+ TTL: 30 * time.Second,
386+ })
387+ a.status = s.(status.StatusCmp)
388+ return a, statusCmd
375389 }
376390 s, _ := a.status.Update(msg)
377391 a.status = s.(status.StatusCmp)
internal/tui/util/util.go+1
@@ -35,6 +35,7 @@ type InfoType int
3535
3636const (
3737 InfoTypeInfo InfoType = iota
38+ InfoTypeSuccess
3839 InfoTypeWarn
3940 InfoTypeError
4041)
internal/update/update.gonew file+118
@@ -0,0 +1,118 @@
1+package update
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "io"
8+ "net/http"
9+ "regexp"
10+ "strings"
11+ "time"
12+)
13+
14+const (
15+ githubApiUrl = "https://api.github.com/repos/charmbracelet/crush/releases/latest"
16+ userAgent = "crush/1.0"
17+)
18+
19+// Default is the default [Client].
20+var Default Client = &github{}
21+
22+// Info contains information about an available update.
23+type Info struct {
24+ Current string
25+ Latest string
26+ URL string
27+}
28+
29+// Matches a version string like:
30+// v0.0.0-0.20251231235959-06c807842604
31+var goInstallRegexp = regexp.MustCompile(`^v?\d+\.\d+\.\d+-\d+\.\d{14}-[0-9a-f]{12}$`)
32+
33+func (i Info) IsDevelopment() bool {
34+ return i.Current == "devel" || i.Current == "unknown" || strings.Contains(i.Current, "dirty") || goInstallRegexp.MatchString(i.Current)
35+}
36+
37+// Available returns true if there's an update available.
38+//
39+// If both current and latest are stable versions, returns true if versions are
40+// different.
41+// If current is a pre-release and latest isn't, returns true.
42+// If latest is a pre-release and current isn't, returns false.
43+func (i Info) Available() bool {
44+ cpr := strings.Contains(i.Current, "-")
45+ lpr := strings.Contains(i.Latest, "-")
46+ // current is pre release && latest isn't a prerelease
47+ if cpr && !lpr {
48+ return true
49+ }
50+ // latest is pre release && current isn't a prerelease
51+ if lpr && !cpr {
52+ return false
53+ }
54+ return i.Current != i.Latest
55+}
56+
57+// Check checks if a new version is available.
58+func Check(ctx context.Context, current string, client Client) (Info, error) {
59+ info := Info{
60+ Current: current,
61+ Latest: current,
62+ }
63+
64+ release, err := client.Latest(ctx)
65+ if err != nil {
66+ return info, fmt.Errorf("failed to fetch latest release: %w", err)
67+ }
68+
69+ info.Latest = strings.TrimPrefix(release.TagName, "v")
70+ info.Current = strings.TrimPrefix(info.Current, "v")
71+ info.URL = release.HTMLURL
72+ return info, nil
73+}
74+
75+// Release represents a GitHub release.
76+type Release struct {
77+ TagName string `json:"tag_name"`
78+ HTMLURL string `json:"html_url"`
79+}
80+
81+// Client is a client that can get the latest release.
82+type Client interface {
83+ Latest(ctx context.Context) (*Release, error)
84+}
85+
86+type github struct{}
87+
88+// Latest implements [Client].
89+func (c *github) Latest(ctx context.Context) (*Release, error) {
90+ client := &http.Client{
91+ Timeout: 30 * time.Second,
92+ }
93+
94+ req, err := http.NewRequestWithContext(ctx, "GET", githubApiUrl, nil)
95+ if err != nil {
96+ return nil, err
97+ }
98+ req.Header.Set("User-Agent", userAgent)
99+ req.Header.Set("Accept", "application/vnd.github.v3+json")
100+
101+ resp, err := client.Do(req)
102+ if err != nil {
103+ return nil, err
104+ }
105+ defer resp.Body.Close()
106+
107+ if resp.StatusCode != http.StatusOK {
108+ body, _ := io.ReadAll(resp.Body)
109+ return nil, fmt.Errorf("github api returned status %d: %s", resp.StatusCode, string(body))
110+ }
111+
112+ var release Release
113+ if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
114+ return nil, err
115+ }
116+
117+ return &release, nil
118+}
internal/update/update_test.gonew file+48
@@ -0,0 +1,48 @@
1+package update
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestCheckForUpdate_Old(t *testing.T) {
11+ info, err := Check(t.Context(), "v0.10.0", testClient{"v0.11.0"})
12+ require.NoError(t, err)
13+ require.NotNil(t, info)
14+ require.True(t, info.Available())
15+}
16+
17+func TestCheckForUpdate_Beta(t *testing.T) {
18+ t.Run("current is stable", func(t *testing.T) {
19+ info, err := Check(t.Context(), "v0.10.0", testClient{"v0.11.0-beta.1"})
20+ require.NoError(t, err)
21+ require.NotNil(t, info)
22+ require.False(t, info.Available())
23+ })
24+
25+ t.Run("current is also beta", func(t *testing.T) {
26+ info, err := Check(t.Context(), "v0.11.0-beta.1", testClient{"v0.11.0-beta.2"})
27+ require.NoError(t, err)
28+ require.NotNil(t, info)
29+ require.True(t, info.Available())
30+ })
31+
32+ t.Run("current is beta, latest isn't", func(t *testing.T) {
33+ info, err := Check(t.Context(), "v0.11.0-beta.1", testClient{"v0.11.0"})
34+ require.NoError(t, err)
35+ require.NotNil(t, info)
36+ require.True(t, info.Available())
37+ })
38+}
39+
40+type testClient struct{ tag string }
41+
42+// Latest implements Client.
43+func (t testClient) Latest(ctx context.Context) (*Release, error) {
44+ return &Release{
45+ TagName: t.tag,
46+ HTMLURL: "https://example.org",
47+ }, nil
48+}
internal/version/version.go+3−7
@@ -4,7 +4,7 @@ import "runtime/debug"
44
55// Build-time parameters set via -ldflags
66
7−var Version = "unknown"
7+var Version = "devel"
88
99// A user may install crush using `go install github.com/charmbracelet/crush@latest`.
1010// without -ldflags, in which case the version above is unset. As a workaround
@@ -13,14 +13,10 @@ var Version = "unknown"
1313func init() {
1414 info, ok := debug.ReadBuildInfo()
1515 if !ok {
16− // < go v1.18
1716 return
1817 }
1918 mainVersion := info.Main.Version
20− if mainVersion == "" || mainVersion == "(devel)" {
21− // bin not built using `go install`
22− return
19+ if mainVersion != "" && mainVersion != "(devel)" {
20+ Version = mainVersion
2321 }
24− // bin built using `go install`
25− Version = mainVersion
2622}
schema.json+1
@@ -468,6 +468,7 @@
468468 "type": "string",
469469 "enum": [
470470 "openai",
471+ "openai-compat",
471472 "anthropic",
472473 "gemini",
473474 "azure",