From afc8fd0be1763d68a6461df1a3b290589ebdd8ad Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Tue, 18 Nov 2025 12:22:40 +0100 Subject: [PATCH 01/13] feat: notify about new crush versions (#361) Signed-off-by: Carlos Alexandro Becker Co-authored-by: Carlos Alexandro Becker Co-authored-by: Andrey Nering --- internal/app/app.go | 19 +++ internal/pubsub/events.go | 6 + internal/tui/components/core/status/status.go | 4 +- internal/tui/tui.go | 11 ++ internal/tui/util/util.go | 1 + internal/update/update.go | 114 ++++++++++++++++++ internal/update/update_test.go | 53 ++++++++ internal/version/version.go | 10 +- 8 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 internal/update/update.go create mode 100644 internal/update/update_test.go diff --git a/internal/app/app.go b/internal/app/app.go index d3e6d2133346df1adc11fc13a612b67cf25b46bd..847eb7161bf1a67ab6611ef566c30072600c000d 100644 --- a/internal/app/app.go +++ b/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,17 @@ 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, + } +} diff --git a/internal/pubsub/events.go b/internal/pubsub/events.go index 2fb0a741353bfc5054641815da9ad3292f49e6a3..27cd47c4061ccfd11e4a9250b1a2fe319e477044 100644 --- a/internal/pubsub/events.go +++ b/internal/pubsub/events.go @@ -26,3 +26,9 @@ type ( Publish(EventType, T) } ) + +// UpdateAvailableMsg is sent when a new version is available. +type UpdateAvailableMsg struct { + CurrentVersion string + LatestVersion string +} diff --git a/internal/tui/components/core/status/status.go b/internal/tui/components/core/status/status.go index 40c5309eb12d9c3361b4108b83f2c5165979eed8..66903704e3effcc800b36222381f05ca1be895aa 100644 --- a/internal/tui/components/core/status/status.go +++ b/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, "…") } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 793421f58307778307d95bda1c3aec13af7522eb..54bec287b2957fe97d377a3e34b9954c38338b71 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -372,6 +372,17 @@ 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) + 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) diff --git a/internal/tui/util/util.go b/internal/tui/util/util.go index 46af5beb8181968bae35356f6d2124561d9f51e7..297a9d36fa47170cae787c82419a17b51fc13b05 100644 --- a/internal/tui/util/util.go +++ b/internal/tui/util/util.go @@ -35,6 +35,7 @@ type InfoType int const ( InfoTypeInfo InfoType = iota + InfoTypeSuccess InfoTypeWarn InfoTypeError ) diff --git a/internal/update/update.go b/internal/update/update.go new file mode 100644 index 0000000000000000000000000000000000000000..061766c3a4eadfe5226dbc95a1afd2dfd677a957 --- /dev/null +++ b/internal/update/update.go @@ -0,0 +1,114 @@ +package update + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "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 +} + +// 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 + if cpr { + // latest isn't a prerelease + if !lpr { + return true + } + } + 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, + } + + if info.Current == "devel" || info.Current == "unknown" { + return info, nil + } + + 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.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 +} diff --git a/internal/update/update_test.go b/internal/update/update_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e833ad220705837a0f18cbc4b6abf6338987644c --- /dev/null +++ b/internal/update/update_test.go @@ -0,0 +1,53 @@ +package update + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCheckForUpdate_DevelopmentVersion(t *testing.T) { + info, err := Check(t.Context(), "unknown", testClient{"v0.11.0"}) + require.NoError(t, err) + require.NotNil(t, info) + require.False(t, info.Available()) +} + +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 +} diff --git a/internal/version/version.go b/internal/version/version.go index 0b616e122dcf4ffb3fbbf4cb7d3b8665300c23ef..6faef3251ca071a0a210ac1bc2327ca848a73ad0 100644 --- a/internal/version/version.go +++ b/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 } From 42122dc2fced97e806c074593e6725b8feed5808 Mon Sep 17 00:00:00 2001 From: Bruno Krugel Date: Tue, 18 Nov 2025 09:43:00 -0300 Subject: [PATCH 02/13] fix: add missing openai-compat in schema (#1461) --- internal/config/config.go | 2 +- schema.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 5cc519c110476683928b8e329a0e4bf1e18c4074..f6bd38d8d93d5bab3fe6bb7b2c16f524b6ffdf87 100644 --- a/internal/config/config.go +++ b/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. diff --git a/schema.json b/schema.json index 9fb49e12fa81d287832fbe5639420e1598711ad5..c9cadc5a51c40c27cfb0b75b9cdae810100714ec 100644 --- a/schema.json +++ b/schema.json @@ -471,7 +471,8 @@ "anthropic", "gemini", "azure", - "vertexai" + "vertexai", + "openai-compat" ], "description": "Provider type that determines the API format", "default": "openai" From 42f0f2b21d60846708f098f021c4a7ac30697ad0 Mon Sep 17 00:00:00 2001 From: Charm <124303983+charmcli@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:43:37 +0000 Subject: [PATCH 03/13] chore: auto-update generated files --- schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schema.json b/schema.json index c9cadc5a51c40c27cfb0b75b9cdae810100714ec..5ba0a700f59e75f89ce147316057930edfe5a453 100644 --- a/schema.json +++ b/schema.json @@ -468,11 +468,11 @@ "type": "string", "enum": [ "openai", + "openai-compat", "anthropic", "gemini", "azure", - "vertexai", - "openai-compat" + "vertexai" ], "description": "Provider type that determines the API format", "default": "openai" From 8f437ac589bae52167870281279b3d7622fe6dc7 Mon Sep 17 00:00:00 2001 From: Bruno Krugel Date: Tue, 18 Nov 2025 12:31:18 -0300 Subject: [PATCH 04/13] fix: don't notify update available when running local build (#1465) --- internal/update/update.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/update/update.go b/internal/update/update.go index 061766c3a4eadfe5226dbc95a1afd2dfd677a957..1ddda8eca17e19077b26c3da09de6c129316f019 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -54,7 +54,7 @@ func Check(ctx context.Context, current string, client Client) (Info, error) { Latest: current, } - if info.Current == "devel" || info.Current == "unknown" { + if info.Current == "devel" || info.Current == "unknown" || strings.Contains(info.Current, "dirty") { return info, nil } @@ -64,6 +64,7 @@ func Check(ctx context.Context, current string, client Client) (Info, error) { } info.Latest = strings.TrimPrefix(release.TagName, "v") + info.Current = strings.TrimPrefix(info.Current, "v") info.URL = release.HTMLURL return info, nil } From fb7d5b6a93336b36e0acc570df29e03dc82969a3 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Tue, 18 Nov 2025 14:43:47 -0300 Subject: [PATCH 05/13] style: small code style updates --- internal/app/app.go | 1 + internal/update/update.go | 16 +++++++--------- internal/update/update_test.go | 2 ++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 847eb7161bf1a67ab6611ef566c30072600c000d..c3748b15070324e9fa438b7ff56f15f76375974a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -400,6 +400,7 @@ func (app *App) Shutdown() { 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 diff --git a/internal/update/update.go b/internal/update/update.go index 1ddda8eca17e19077b26c3da09de6c129316f019..0a3178d818207f11a7f46560065c4d47e8a1f5e7 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -11,7 +11,7 @@ import ( ) const ( - githubAPIURL = "https://api.github.com/repos/charmbracelet/crush/releases/latest" + githubApiUrl = "https://api.github.com/repos/charmbracelet/crush/releases/latest" userAgent = "crush/1.0" ) @@ -34,13 +34,11 @@ type Info struct { func (i Info) Available() bool { cpr := strings.Contains(i.Current, "-") lpr := strings.Contains(i.Latest, "-") - // current is pre release - if cpr { - // latest isn't a prerelease - if !lpr { - return true - } + // 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 } @@ -88,7 +86,7 @@ func (c *github) Latest(ctx context.Context) (*Release, error) { Timeout: 30 * time.Second, } - req, err := http.NewRequestWithContext(ctx, "GET", githubAPIURL, nil) + req, err := http.NewRequestWithContext(ctx, "GET", githubApiUrl, nil) if err != nil { return nil, err } @@ -103,7 +101,7 @@ func (c *github) Latest(ctx context.Context) (*Release, error) { if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("github api returned status %d: %s", resp.StatusCode, string(body)) } var release Release diff --git a/internal/update/update_test.go b/internal/update/update_test.go index e833ad220705837a0f18cbc4b6abf6338987644c..488c7dfd4b29dec4dfe5d13958c13dcb914e5961 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -28,12 +28,14 @@ func TestCheckForUpdate_Beta(t *testing.T) { 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) From 12f093d83e7dbd75df725d661959a3887868627f Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Tue, 18 Nov 2025 14:52:38 -0300 Subject: [PATCH 06/13] feat: show a different message if crush is built from source --- internal/app/app.go | 1 + internal/pubsub/events.go | 1 + internal/tui/tui.go | 3 +++ internal/update/update.go | 8 ++++---- internal/update/update_test.go | 7 ------- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index c3748b15070324e9fa438b7ff56f15f76375974a..8519f258502ad10f146870b89c7bb5f0f50994e4 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -408,5 +408,6 @@ func (app *App) checkForUpdates(ctx context.Context) { app.events <- pubsub.UpdateAvailableMsg{ CurrentVersion: info.Current, LatestVersion: info.Latest, + IsDevelopment: info.IsDevelopment(), } } diff --git a/internal/pubsub/events.go b/internal/pubsub/events.go index 27cd47c4061ccfd11e4a9250b1a2fe319e477044..dadecaa14545337404c15000da37fbd5901932df 100644 --- a/internal/pubsub/events.go +++ b/internal/pubsub/events.go @@ -31,4 +31,5 @@ type ( type UpdateAvailableMsg struct { CurrentVersion string LatestVersion string + IsDevelopment bool } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 54bec287b2957fe97d377a3e34b9954c38338b71..e4eea700a4c109fa291fbaad3dc764e6df23921b 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -376,6 +376,9 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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, diff --git a/internal/update/update.go b/internal/update/update.go index 0a3178d818207f11a7f46560065c4d47e8a1f5e7..b4982996126eb5d95d7f9acf22721ddc7378aedc 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -25,6 +25,10 @@ type Info struct { URL string } +func (i Info) IsDevelopment() bool { + return i.Current == "devel" || i.Current == "unknown" || strings.Contains(i.Current, "dirty") +} + // Available returns true if there's an update available. // // If both current and latest are stable versions, returns true if versions are @@ -52,10 +56,6 @@ func Check(ctx context.Context, current string, client Client) (Info, error) { Latest: current, } - if info.Current == "devel" || info.Current == "unknown" || strings.Contains(info.Current, "dirty") { - return info, nil - } - release, err := client.Latest(ctx) if err != nil { return info, fmt.Errorf("failed to fetch latest release: %w", err) diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 488c7dfd4b29dec4dfe5d13958c13dcb914e5961..87e3849eb5a9ddc06b1e22c15c0bdde0b7739085 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -7,13 +7,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestCheckForUpdate_DevelopmentVersion(t *testing.T) { - info, err := Check(t.Context(), "unknown", testClient{"v0.11.0"}) - require.NoError(t, err) - require.NotNil(t, info) - require.False(t, info.Available()) -} - func TestCheckForUpdate_Old(t *testing.T) { info, err := Check(t.Context(), "v0.10.0", testClient{"v0.11.0"}) require.NoError(t, err) From c38183438c27b1eea10e413b554c93bc1a884c83 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 19 Nov 2025 13:02:18 +0100 Subject: [PATCH 07/13] fix: handle google reasoning (#1474) --- go.mod | 7 ++++--- go.sum | 14 ++++++++------ internal/agent/agent.go | 2 +- internal/message/content.go | 5 ++++- internal/shell/background_test.go | 1 + 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 32d267f1538d79d5de83038ffa94c4df0d5e9f81..f8c69810e844780a76445b55036098c63757f71e 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index bc7cd371e8c4db0cf96f2905ac2df50836b8ca50..1458d7a78717609e015f21ea9d52b3b45a6df4dc 100644 --- a/go.sum +++ b/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= diff --git a/internal/agent/agent.go b/internal/agent/agent.go index f4d8af377934245bd1c94f7cadf61bbadf60dd44..ec5bc19ba4efaf0cc15f46620711621a92dff2b9 100644 --- a/internal/agent/agent.go +++ b/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 { diff --git a/internal/message/content.go b/internal/message/content.go index 7f35678230759ab3dcfc13287d340d6f0327d722..358ad120d8f87109ea8888984ad236b155388788 100644 --- a/internal/message/content.go +++ b/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) diff --git a/internal/shell/background_test.go b/internal/shell/background_test.go index 10c67625340c3a38fbb89b3c9eafa1e3d303b514..8d0ba200519887765ba33e2d79a2b78fe6edd7d9 100644 --- a/internal/shell/background_test.go +++ b/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() From f08e8525207b456dea90d90a0bdb993564f6a62e Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 19 Nov 2025 12:07:24 -0300 Subject: [PATCH 09/13] fix: detect version for `go install ...@main` (#1476) --- internal/update/update.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/update/update.go b/internal/update/update.go index b4982996126eb5d95d7f9acf22721ddc7378aedc..a813fe3516dc28233e3df01c77d4d62d4d97db18 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "regexp" "strings" "time" ) @@ -25,8 +26,12 @@ type Info struct { 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") + 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. From 4827f4cda3ed74068b446250ab204db69f1cf7d0 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 19 Nov 2025 16:41:41 +0100 Subject: [PATCH 10/13] fix: kimi coding api key validation (#1477) --- internal/config/config.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index f6bd38d8d93d5bab3fe6bb7b2c16f524b6ffdf87..4b441b0b18563814ca58b9d48cf2e8ffbd7e782f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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: From 14e157ac04adbdd1228acd7e81aec68ca097e5b5 Mon Sep 17 00:00:00 2001 From: Charm <124303983+charmcli@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:59:21 -0300 Subject: [PATCH 11/13] chore(legal): @micahwalter has signed the CLA --- .github/cla-signatures.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/cla-signatures.json b/.github/cla-signatures.json index 829ed35fd5fad02b4c32a6d0f00300f07f955413..435347c246578e4158818c36032fa318c1b1fa21 100644 --- a/.github/cla-signatures.json +++ b/.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 } ] } \ No newline at end of file From 281e7fa203f378fda7f0ab59dfa87eebfd8daed1 Mon Sep 17 00:00:00 2001 From: Micah Walter <47419+micahwalter@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:05:39 -0500 Subject: [PATCH 13/13] docs(readme): update "aws bedrock" to "amazon bedrock" (#1478) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 85f7376d7a9d30ecb55f26ce159ada1120ae64fa..8be0e3b0f8e23e0fa59b5095fe3892801aa17b35 100644 --- a/README.md +++ b/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 |