Merge branch 'main' into ui

Ayman Bagabas created

Change summary

.github/cla-signatures.json                   |   8 +
README.md                                     |  10 
go.mod                                        |   7 
go.sum                                        |  14 +-
internal/agent/agent.go                       |   2 
internal/app/app.go                           |  21 +++
internal/config/config.go                     |   6 
internal/message/content.go                   |   5 
internal/pubsub/events.go                     |   7 +
internal/shell/background_test.go             |   1 
internal/tui/components/core/status/status.go |   4 
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                   |  10 -
schema.json                                   |   1 
17 files changed, 251 insertions(+), 26 deletions(-)

Detailed changes

.github/cla-signatures.json 🔗

@@ -863,6 +863,14 @@
       "created_at": "2025-11-16T20:55:47Z",
       "repoId": 987670088,
       "pullRequestNo": 1457
+    },
+    {
+      "name": "micahwalter",
+      "id": 47419,
+      "comment_id": 3553482361,
+      "created_at": "2025-11-19T15:59:07Z",
+      "repoId": 987670088,
+      "pullRequestNo": 1478
     }
   ]
 }

README.md 🔗

@@ -186,11 +186,11 @@ That said, you can also set environment variables for preferred providers.
 | `VERTEXAI_PROJECT`          | Google Cloud VertexAI (Gemini)                     |
 | `VERTEXAI_LOCATION`         | Google Cloud VertexAI (Gemini)                     |
 | `GROQ_API_KEY`              | Groq                                               |
-| `AWS_ACCESS_KEY_ID`         | AWS Bedrock (Claude)                               |
-| `AWS_SECRET_ACCESS_KEY`     | AWS Bedrock (Claude)                               |
-| `AWS_REGION`                | AWS Bedrock (Claude)                               |
-| `AWS_PROFILE`               | AWS Bedrock (Custom Profile)                       |
-| `AWS_BEARER_TOKEN_BEDROCK`  | AWS Bedrock                                        |
+| `AWS_ACCESS_KEY_ID`         | Amazon Bedrock (Claude)                               |
+| `AWS_SECRET_ACCESS_KEY`     | Amazon Bedrock (Claude)                               |
+| `AWS_REGION`                | Amazon Bedrock (Claude)                               |
+| `AWS_PROFILE`               | Amazon Bedrock (Custom Profile)                       |
+| `AWS_BEARER_TOKEN_BEDROCK`  | Amazon Bedrock                                        |
 | `AZURE_OPENAI_API_ENDPOINT` | Azure OpenAI models                                |
 | `AZURE_OPENAI_API_KEY`      | Azure OpenAI models (optional when using Entra ID) |
 | `AZURE_OPENAI_API_VERSION`  | Azure OpenAI models                                |

go.mod 🔗

@@ -5,7 +5,7 @@ go 1.25.0
 require (
 	charm.land/bubbles/v2 v2.0.0-rc.1
 	charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251117161017-15f884bd2973
-	charm.land/fantasy v0.3.1
+	charm.land/fantasy v0.3.2
 	charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410
 	charm.land/x/vcr v0.1.1
 	github.com/JohannesKaufmann/html-to-markdown v1.6.0
@@ -26,7 +26,7 @@ require (
 	github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
 	github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
 	github.com/charmbracelet/x/exp/ordered v0.1.0
-	github.com/charmbracelet/x/exp/slice v0.0.0-20251113172435-cef867b85f6a
+	github.com/charmbracelet/x/exp/slice v0.0.0-20251118172736-77d017256798
 	github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4
 	github.com/charmbracelet/x/term v0.2.2
 	github.com/denisbrodbeck/machineid v1.0.1
@@ -116,7 +116,8 @@ require (
 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/kaptinlin/go-i18n v0.2.0 // indirect
-	github.com/kaptinlin/jsonschema v0.5.2 // indirect
+	github.com/kaptinlin/jsonpointer v0.4.6 // indirect
+	github.com/kaptinlin/jsonschema v0.6.1 // indirect
 	github.com/kaptinlin/messageformat-go v0.4.6 // indirect
 	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.0.9 // indirect

go.sum 🔗

@@ -2,8 +2,8 @@ charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM
 charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
 charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251117161017-15f884bd2973 h1:Ay8VWyn/CbwltswomzWXj0m5KKfSJavFfCDCxI+j8qo=
 charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251117161017-15f884bd2973/go.mod h1:IXFmnCnMLTWw/KQ9rEatSYqbAPAYi8kA3Yqwa1SFnLk=
-charm.land/fantasy v0.3.1 h1:YeMoLnaOHM3hdXq+SByxIKZxdm/2CHgKIS7HE0k/G6I=
-charm.land/fantasy v0.3.1/go.mod h1:sV8Ns/JTJHOaYOHPgVRDugMheAyxsW/nmdpVGrycYEk=
+charm.land/fantasy v0.3.2 h1:yHTsSZ25LcICMRw3xzdz3OkaZtDQch+B5ljJo17HxgU=
+charm.land/fantasy v0.3.2/go.mod h1:sV8Ns/JTJHOaYOHPgVRDugMheAyxsW/nmdpVGrycYEk=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
 charm.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
 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
 github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
 github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
-github.com/charmbracelet/x/exp/slice v0.0.0-20251113172435-cef867b85f6a h1:+mXWbAiS5wNq8VvUd+/P4STqdu2dLtCe9sFr9IqdPDk=
-github.com/charmbracelet/x/exp/slice v0.0.0-20251113172435-cef867b85f6a/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
+github.com/charmbracelet/x/exp/slice v0.0.0-20251118172736-77d017256798 h1:EkOQR1G3MhyPxA39njT7E33V1Y/bDbF1XxEcMmM6Ox8=
+github.com/charmbracelet/x/exp/slice v0.0.0-20251118172736-77d017256798/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
 github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ=
 github.com/charmbracelet/x/json v0.2.0/go.mod h1:opFIflx2YgXgi49xVUu8gEQ21teFAxyMwvOiZhIvWNM=
 github.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
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/kaptinlin/go-i18n v0.2.0 h1:8iwjAERQbCVF78c3HxC4MxUDxDRFvQVQlMDvlsO43hU=
 github.com/kaptinlin/go-i18n v0.2.0/go.mod h1:gRHEMrTHtQLsAFwulPbJG71TwHjXxkagn88O8FI8FuA=
-github.com/kaptinlin/jsonschema v0.5.2 h1:ipUBEv1/RnT+ErwdqXZ3Xtwkwp6uqp/Q9lFILrwhUfc=
-github.com/kaptinlin/jsonschema v0.5.2/go.mod h1:HuWb90460GwFxRe0i9Ni3Z7YXwkjpqjeccWTB9gTZZE=
+github.com/kaptinlin/jsonpointer v0.4.6 h1:hAett1YROLwxAOKZS08hsJueXr1w0fTMSvWq2x1IoUA=
+github.com/kaptinlin/jsonpointer v0.4.6/go.mod h1:5pHXLIYd2FgV0rUEsChp6xTOvcC2OFk7kF/cjhHzL4g=
+github.com/kaptinlin/jsonschema v0.6.1 h1:RNUQ11ZCHTtM80YcVwRm033H5OJS+MpO06d9x7Yk25o=
+github.com/kaptinlin/jsonschema v0.6.1/go.mod h1:T8SNWNTRLDS1w+ogMZpGYqIfUXn/8DK9r06mf8XbNLE=
 github.com/kaptinlin/messageformat-go v0.4.6 h1:57DUC9en40mGZR7MvqOS+5EYogAl465fjo+loAA1KPg=
 github.com/kaptinlin/messageformat-go v0.4.6/go.mod h1:r0PH7FsxJX8jS/n6LAYZon5w3X+yfCLUrquqYd2H7ks=
 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=

internal/agent/agent.go 🔗

@@ -273,7 +273,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 			}
 			if googleData, ok := reasoning.ProviderMetadata[google.Name]; ok {
 				if reasoning, ok := googleData.(*google.ReasoningMetadata); ok {
-					currentAssistant.AppendReasoningSignature(reasoning.Signature)
+					currentAssistant.AppendThoughtSignature(reasoning.Signature, reasoning.ToolID)
 				}
 			}
 			if openaiData, ok := reasoning.ProviderMetadata[openai.Name]; ok {

internal/app/app.go 🔗

@@ -33,6 +33,8 @@ import (
 	"github.com/charmbracelet/crush/internal/term"
 	"github.com/charmbracelet/crush/internal/tui/components/anim"
 	"github.com/charmbracelet/crush/internal/tui/styles"
+	"github.com/charmbracelet/crush/internal/update"
+	"github.com/charmbracelet/crush/internal/version"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/charmtone"
 )
@@ -92,6 +94,9 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
 	// Initialize LSP clients in the background.
 	app.initLSPClients(ctx)
 
+	// Check for updates in the background.
+	go app.checkForUpdates(ctx)
+
 	go func() {
 		slog.Info("Initializing MCP clients")
 		mcp.Initialize(ctx, app.Permissions, cfg)
@@ -390,3 +395,19 @@ func (app *App) Shutdown() {
 		}
 	}
 }
+
+// checkForUpdates checks for available updates.
+func (app *App) checkForUpdates(ctx context.Context) {
+	checkCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+
+	info, err := update.Check(checkCtx, version.Version, update.Default)
+	if err != nil || !info.Available() {
+		return
+	}
+	app.events <- pubsub.UpdateAvailableMsg{
+		CurrentVersion: info.Current,
+		LatestVersion:  info.Latest,
+		IsDevelopment:  info.IsDevelopment(),
+	}
+}

internal/config/config.go 🔗

@@ -89,7 +89,7 @@ type ProviderConfig struct {
 	// The provider's API endpoint.
 	BaseURL string `json:"base_url,omitempty" jsonschema:"description=Base URL for the provider's API,format=uri,example=https://api.openai.com/v1"`
 	// The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
-	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"`
+	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"`
 	// The provider's API key.
 	APIKey string `json:"api_key,omitempty" jsonschema:"description=API key for authentication with the provider,example=$OPENAI_API_KEY"`
 	// Marks the provider as disabled.
@@ -635,6 +635,10 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
 			baseURL = "https://api.anthropic.com/v1"
 		}
 		testURL = baseURL + "/models"
+		// TODO: replace with const when catwalk is released
+		if c.ID == "kimi-coding" {
+			testURL = baseURL + "/v1/models"
+		}
 		headers["x-api-key"] = apiKey
 		headers["anthropic-version"] = "2023-06-01"
 	case catwalk.TypeGoogle:

internal/message/content.go 🔗

@@ -45,6 +45,7 @@ type ReasoningContent struct {
 	Thinking         string                             `json:"thinking"`
 	Signature        string                             `json:"signature"`
 	ThoughtSignature string                             `json:"thought_signature"` // Used for google
+	ToolID           string                             `json:"tool_id"`           // Used for openrouter google models
 	ResponsesData    *openai.ResponsesReasoningMetadata `json:"responses_data"`
 	StartedAt        int64                              `json:"started_at,omitempty"`
 	FinishedAt       int64                              `json:"finished_at,omitempty"`
@@ -261,12 +262,13 @@ func (m *Message) AppendReasoningContent(delta string) {
 	}
 }
 
-func (m *Message) AppendThoughtSignature(signature string) {
+func (m *Message) AppendThoughtSignature(signature string, toolCallID string) {
 	for i, part := range m.Parts {
 		if c, ok := part.(ReasoningContent); ok {
 			m.Parts[i] = ReasoningContent{
 				Thinking:         c.Thinking,
 				ThoughtSignature: c.ThoughtSignature + signature,
+				ToolID:           toolCallID,
 				Signature:        c.Signature,
 				StartedAt:        c.StartedAt,
 				FinishedAt:       c.FinishedAt,
@@ -464,6 +466,7 @@ func (m *Message) ToAIMessage() []fantasy.Message {
 			if reasoning.ThoughtSignature != "" {
 				reasoningPart.ProviderOptions[google.Name] = &google.ReasoningMetadata{
 					Signature: reasoning.ThoughtSignature,
+					ToolID:    reasoning.ToolID,
 				}
 			}
 			parts = append(parts, reasoningPart)

internal/pubsub/events.go 🔗

@@ -26,3 +26,10 @@ type (
 		Publish(EventType, T)
 	}
 )
+
+// UpdateAvailableMsg is sent when a new version is available.
+type UpdateAvailableMsg struct {
+	CurrentVersion string
+	LatestVersion  string
+	IsDevelopment  bool
+}

internal/shell/background_test.go 🔗

@@ -8,6 +8,7 @@ import (
 )
 
 func TestBackgroundShellManager_Start(t *testing.T) {
+	t.Skip("Skipping this until I figure out why its flaky")
 	t.Parallel()
 
 	ctx := context.Background()

internal/tui/components/core/status/status.go 🔗

@@ -82,10 +82,10 @@ func (m *statusCmp) infoMsg() string {
 		info := ansi.Truncate(m.info.Msg, widthLeft, "…")
 		message = t.S().Base.Foreground(t.BgOverlay).Width(widthLeft+2).Background(t.Warning).Padding(0, 1).Render(info)
 	default:
-		infoType = t.S().Base.Foreground(t.BgOverlay).Background(t.Green).Padding(0, 1).Render("OKAY!")
+		infoType = t.S().Base.Foreground(t.BgSubtle).Background(t.Green).Padding(0, 1).Bold(true).Render("HEY!")
 		widthLeft := m.width - (lipgloss.Width(infoType) + 2)
 		info := ansi.Truncate(m.info.Msg, widthLeft, "…")
-		message = t.S().Base.Background(t.Success).Width(widthLeft+2).Foreground(t.White).Padding(0, 1).Render(info)
+		message = t.S().Base.Background(t.GreenDark).Width(widthLeft+2).Foreground(t.BgSubtle).Padding(0, 1).Render(info)
 	}
 	return ansi.Truncate(infoType+message, m.width, "…")
 }

internal/tui/tui.go 🔗

@@ -372,6 +372,20 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			cmds = append(cmds, pageCmd)
 		}
 		return a, tea.Batch(cmds...)
+	// Update Available
+	case pubsub.UpdateAvailableMsg:
+		// Show update notification in status bar
+		statusMsg := fmt.Sprintf("Crush update available: v%s → v%s.", msg.CurrentVersion, msg.LatestVersion)
+		if msg.IsDevelopment {
+			statusMsg = fmt.Sprintf("This is a development version of Crush. The latest version is v%s.", msg.LatestVersion)
+		}
+		s, statusCmd := a.status.Update(util.InfoMsg{
+			Type: util.InfoTypeInfo,
+			Msg:  statusMsg,
+			TTL:  30 * time.Second,
+		})
+		a.status = s.(status.StatusCmp)
+		return a, statusCmd
 	}
 	s, _ := a.status.Update(msg)
 	a.status = s.(status.StatusCmp)

internal/tui/util/util.go 🔗

@@ -35,6 +35,7 @@ type InfoType int
 
 const (
 	InfoTypeInfo InfoType = iota
+	InfoTypeSuccess
 	InfoTypeWarn
 	InfoTypeError
 )

internal/update/update.go 🔗

@@ -0,0 +1,118 @@
+package update
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"regexp"
+	"strings"
+	"time"
+)
+
+const (
+	githubApiUrl = "https://api.github.com/repos/charmbracelet/crush/releases/latest"
+	userAgent    = "crush/1.0"
+)
+
+// Default is the default [Client].
+var Default Client = &github{}
+
+// Info contains information about an available update.
+type Info struct {
+	Current string
+	Latest  string
+	URL     string
+}
+
+// Matches a version string like:
+// v0.0.0-0.20251231235959-06c807842604
+var goInstallRegexp = regexp.MustCompile(`^v?\d+\.\d+\.\d+-\d+\.\d{14}-[0-9a-f]{12}$`)
+
+func (i Info) IsDevelopment() bool {
+	return i.Current == "devel" || i.Current == "unknown" || strings.Contains(i.Current, "dirty") || goInstallRegexp.MatchString(i.Current)
+}
+
+// Available returns true if there's an update available.
+//
+// If both current and latest are stable versions, returns true if versions are
+// different.
+// If current is a pre-release and latest isn't, returns true.
+// If latest is a pre-release and current isn't, returns false.
+func (i Info) Available() bool {
+	cpr := strings.Contains(i.Current, "-")
+	lpr := strings.Contains(i.Latest, "-")
+	// current is pre release && latest isn't a prerelease
+	if cpr && !lpr {
+		return true
+	}
+	// latest is pre release && current isn't a prerelease
+	if lpr && !cpr {
+		return false
+	}
+	return i.Current != i.Latest
+}
+
+// Check checks if a new version is available.
+func Check(ctx context.Context, current string, client Client) (Info, error) {
+	info := Info{
+		Current: current,
+		Latest:  current,
+	}
+
+	release, err := client.Latest(ctx)
+	if err != nil {
+		return info, fmt.Errorf("failed to fetch latest release: %w", err)
+	}
+
+	info.Latest = strings.TrimPrefix(release.TagName, "v")
+	info.Current = strings.TrimPrefix(info.Current, "v")
+	info.URL = release.HTMLURL
+	return info, nil
+}
+
+// Release represents a GitHub release.
+type Release struct {
+	TagName string `json:"tag_name"`
+	HTMLURL string `json:"html_url"`
+}
+
+// Client is a client that can get the latest release.
+type Client interface {
+	Latest(ctx context.Context) (*Release, error)
+}
+
+type github struct{}
+
+// Latest implements [Client].
+func (c *github) Latest(ctx context.Context) (*Release, error) {
+	client := &http.Client{
+		Timeout: 30 * time.Second,
+	}
+
+	req, err := http.NewRequestWithContext(ctx, "GET", githubApiUrl, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("User-Agent", userAgent)
+	req.Header.Set("Accept", "application/vnd.github.v3+json")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		body, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("github api returned status %d: %s", resp.StatusCode, string(body))
+	}
+
+	var release Release
+	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
+		return nil, err
+	}
+
+	return &release, nil
+}

internal/update/update_test.go 🔗

@@ -0,0 +1,48 @@
+package update
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestCheckForUpdate_Old(t *testing.T) {
+	info, err := Check(t.Context(), "v0.10.0", testClient{"v0.11.0"})
+	require.NoError(t, err)
+	require.NotNil(t, info)
+	require.True(t, info.Available())
+}
+
+func TestCheckForUpdate_Beta(t *testing.T) {
+	t.Run("current is stable", func(t *testing.T) {
+		info, err := Check(t.Context(), "v0.10.0", testClient{"v0.11.0-beta.1"})
+		require.NoError(t, err)
+		require.NotNil(t, info)
+		require.False(t, info.Available())
+	})
+
+	t.Run("current is also beta", func(t *testing.T) {
+		info, err := Check(t.Context(), "v0.11.0-beta.1", testClient{"v0.11.0-beta.2"})
+		require.NoError(t, err)
+		require.NotNil(t, info)
+		require.True(t, info.Available())
+	})
+
+	t.Run("current is beta, latest isn't", func(t *testing.T) {
+		info, err := Check(t.Context(), "v0.11.0-beta.1", testClient{"v0.11.0"})
+		require.NoError(t, err)
+		require.NotNil(t, info)
+		require.True(t, info.Available())
+	})
+}
+
+type testClient struct{ tag string }
+
+// Latest implements Client.
+func (t testClient) Latest(ctx context.Context) (*Release, error) {
+	return &Release{
+		TagName: t.tag,
+		HTMLURL: "https://example.org",
+	}, nil
+}

internal/version/version.go 🔗

@@ -4,7 +4,7 @@ import "runtime/debug"
 
 // Build-time parameters set via -ldflags
 
-var Version = "unknown"
+var Version = "devel"
 
 // A user may install crush using `go install github.com/charmbracelet/crush@latest`.
 // without -ldflags, in which case the version above is unset. As a workaround
@@ -13,14 +13,10 @@ var Version = "unknown"
 func init() {
 	info, ok := debug.ReadBuildInfo()
 	if !ok {
-		// < go v1.18
 		return
 	}
 	mainVersion := info.Main.Version
-	if mainVersion == "" || mainVersion == "(devel)" {
-		// bin not built using `go install`
-		return
+	if mainVersion != "" && mainVersion != "(devel)" {
+		Version = mainVersion
 	}
-	// bin built using `go install`
-	Version = mainVersion
 }

schema.json 🔗

@@ -468,6 +468,7 @@
           "type": "string",
           "enum": [
             "openai",
+            "openai-compat",
             "anthropic",
             "gemini",
             "azure",