diff --git a/README.md b/README.md index a2b07093c1e45162018614411ba7a4300f9ef680..7f28c5c049cdb6c45bc83ec59f94f4310c13b7c5 100644 --- a/README.md +++ b/README.md @@ -545,7 +545,7 @@ config: } ``` -## Disabling Provider Auto-Updates +## Provider Auto-Updates By default, Crush automatically checks for the latest and greatest list of providers and models from [Catwalk](https://github.com/charmbracelet/catwalk), @@ -553,6 +553,8 @@ the open source Crush provider database. This means that when new providers and models are available, or when model metadata changes, Crush automatically updates your local configuration. +### Disabling automatic provider updates + For those with restricted internet access, or those who prefer to work in air-gapped environments, this might not be want you want, and this feature can be disabled. @@ -597,6 +599,36 @@ crush update-providers embedded crush update-providers --help ``` +## Metrics + +Crush records pseudonymous usage metrics (tied to a device-specific hash), +which maintainers rely on to inform development and support priorities. The +metrics include solely usage metadata; prompts and responses are NEVER +collected. + +Details on exactly what’s collected are in the source code ([here](https://github.com/charmbracelet/crush/tree/main/internal/event) +and [here](https://github.com/charmbracelet/crush/blob/main/internal/llm/agent/event.go)). + +You can opt out of metrics collection at any time by setting the environment +variable by setting the following in your environment: + +```bash +export CRUSH_DISABLE_METRICS=1 +``` + +Or by setting the following in your config: + +```json +{ + "options": { + "disable_metrics": true + } +} +``` + +Crush also respects the [`DO_NOT_TRACK`](https://consoledonottrack.com) +convention which can be enabled via `export DO_NOT_TRACK=1`. + ## A Note on Claude Max and GitHub Copilot Crush only supports model providers through official, compliant APIs. We do not diff --git a/go.mod b/go.mod index b59f4d649ad9c635d5dc8d583e993208b06547a6..ea62993931c7532b55127f54b35bab0be2eb23a7 100644 --- a/go.mod +++ b/go.mod @@ -81,6 +81,7 @@ require ( github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/denisbrodbeck/machineid v1.0.1 github.com/disintegration/gift v1.1.2 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -95,6 +96,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect @@ -114,6 +116,7 @@ require ( github.com/ncruces/julianday v1.0.0 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/posthog/posthog-go v1.6.10 github.com/rivo/uniseg v0.4.7 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect diff --git a/go.sum b/go.sum index a921201c472e338f3c068503d9404d68a7bcba12..fedfeb243142a92fe79d7d6b474ca921dbc952bd 100644 --- a/go.sum +++ b/go.sum @@ -118,6 +118,8 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= +github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs= github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg= @@ -162,6 +164,8 @@ github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -233,6 +237,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posthog/posthog-go v1.6.10 h1:OA6bkiUg89rI7f5cSXbcrH5+wLinyS6hHplnD92Pu/M= +github.com/posthog/posthog-go v1.6.10/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY= github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng= github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/qjebbs/go-jsons v1.0.0-alpha.4 h1:Qsb4ohRUHQODIUAsJKdKJ/SIDbsO7oGOzsfy+h1yQZs= diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 3ecb23e5acd68c1666cf9798b17bcc408b9290e1..ea9c218b67c65815b6bcc2c8b1cb17fd02390b39 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -7,11 +7,13 @@ import ( "log/slog" "os" "path/filepath" + "strconv" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/app" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/db" + "github.com/charmbracelet/crush/internal/event" "github.com/charmbracelet/crush/internal/tui" "github.com/charmbracelet/crush/internal/version" "github.com/charmbracelet/fang" @@ -66,6 +68,8 @@ crush -y } defer app.Shutdown() + event.AppInitialized() + // Set up the TUI. program := tea.NewProgram( tui.New(app), @@ -78,11 +82,15 @@ crush -y go app.Subscribe(program) if _, err := program.Run(); err != nil { + event.Error(err) slog.Error("TUI run error", "error", err) return fmt.Errorf("TUI error: %v", err) } return nil }, + PostRun: func(cmd *cobra.Command, args []string) { + event.AppExited() + }, } func Execute() { @@ -135,9 +143,26 @@ func setupApp(cmd *cobra.Command) (*app.App, error) { return nil, err } + if shouldEnableMetrics() { + event.Init() + } + return appInstance, nil } +func shouldEnableMetrics() bool { + if v, _ := strconv.ParseBool(os.Getenv("CRUSH_DISABLE_METRICS")); v { + return false + } + if v, _ := strconv.ParseBool(os.Getenv("DO_NOT_TRACK")); v { + return false + } + if config.Get().Options.DisableMetrics { + return false + } + return true +} + func MaybePrependStdin(prompt string) (string, error) { if term.IsTerminal(os.Stdin.Fd()) { return prompt, nil diff --git a/internal/config/config.go b/internal/config/config.go index 8e4b8e5437e31af351b14b7330ab1bf4326b4863..3578850d228b78503e67e630a9b688c575403b9c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -153,6 +153,7 @@ type Options struct { DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"` DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"` Attribution *Attribution `json:"attribution,omitempty" jsonschema:"description=Attribution settings for generated content"` + DisableMetrics bool `json:"disable_metrics,omitempty" jsonschema:"description=Disable sending metrics,default=false"` } type MCPs map[string]MCPConfig diff --git a/internal/event/all.go b/internal/event/all.go new file mode 100644 index 0000000000000000000000000000000000000000..8caf98e62ff3f39b291e341959ebc943361eec05 --- /dev/null +++ b/internal/event/all.go @@ -0,0 +1,59 @@ +package event + +import ( + "time" +) + +var appStartTime time.Time + +func AppInitialized() { + appStartTime = time.Now() + send("app initialized") +} + +func AppExited() { + duration := time.Since(appStartTime).Truncate(time.Second) + send( + "app exited", + "app duration pretty", duration.String(), + "app duration in seconds", int64(duration.Seconds()), + ) + Flush() +} + +func SessionCreated() { + send("session created") +} + +func SessionDeleted() { + send("session deleted") +} + +func SessionSwitched() { + send("session switched") +} + +func FilePickerOpened() { + send("filepicker opened") +} + +func PromptSent(props ...any) { + send( + "prompt sent", + props..., + ) +} + +func PromptResponded(props ...any) { + send( + "prompt responded", + props..., + ) +} + +func TokensUsed(props ...any) { + send( + "tokens used", + props..., + ) +} diff --git a/internal/event/event.go b/internal/event/event.go new file mode 100644 index 0000000000000000000000000000000000000000..89a02411eefbbdefc94e784abbca7e3cd638027d --- /dev/null +++ b/internal/event/event.go @@ -0,0 +1,114 @@ +package event + +import ( + "fmt" + "log/slog" + "os" + "reflect" + "runtime" + + "github.com/charmbracelet/crush/internal/version" + "github.com/denisbrodbeck/machineid" + "github.com/posthog/posthog-go" +) + +const ( + endpoint = "https://data.charm.land" + key = "phc_4zt4VgDWLqbYnJYEwLRxFoaTL2noNrQij0C6E8k3I0V" +) + +var ( + client posthog.Client + + baseProps = posthog.NewProperties(). + Set("GOOS", runtime.GOOS). + Set("GOARCH", runtime.GOARCH). + Set("TERM", os.Getenv("TERM")). + Set("SHELL", os.Getenv("SHELL")). + Set("Version", version.Version). + Set("GoVersion", runtime.Version()) +) + +func Init() { + c, err := posthog.NewWithConfig(key, posthog.Config{ + Endpoint: endpoint, + Logger: logger{}, + }) + if err != nil { + slog.Error("Failed to initialize PostHog client", "error", err) + } + client = c +} + +// send logs an event to PostHog with the given event name and properties. +func send(event string, props ...any) { + if client == nil { + return + } + err := client.Enqueue(posthog.Capture{ + DistinctId: distinctId(), + Event: event, + Properties: pairsToProps(props...).Merge(baseProps), + }) + if err != nil { + slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err) + return + } +} + +// Error logs an error event to PostHog with the error type and message. +func Error(err any, props ...any) { + if client == nil { + return + } + // The PostHog Go client does not yet support sending exceptions. + // We're mimicking the behavior by sending the minimal info required + // for PostHog to recognize this as an exception event. + props = append( + []any{ + "$exception_list", + []map[string]string{ + {"type": reflect.TypeOf(err).String(), "value": fmt.Sprintf("%v", err)}, + }, + }, + props..., + ) + send("$exception", props...) +} + +func Flush() { + if client == nil { + return + } + if err := client.Close(); err != nil { + slog.Error("Failed to flush PostHog events", "error", err) + } +} + +func pairsToProps(props ...any) posthog.Properties { + p := posthog.NewProperties() + + if !isEven(len(props)) { + slog.Error("Event properties must be provided as key-value pairs", "props", props) + return p + } + + for i := 0; i < len(props); i += 2 { + key := props[i].(string) + value := props[i+1] + p = p.Set(key, value) + } + return p +} + +func isEven(n int) bool { + return n%2 == 0 +} + +func distinctId() string { + id, err := machineid.ProtectedID("charm") + if err != nil { + return "crush-cli" + } + return id +} diff --git a/internal/event/logger.go b/internal/event/logger.go new file mode 100644 index 0000000000000000000000000000000000000000..7648ae2c2cca91ed20535c0d65a677cd4db84500 --- /dev/null +++ b/internal/event/logger.go @@ -0,0 +1,27 @@ +package event + +import ( + "log/slog" + + "github.com/posthog/posthog-go" +) + +var _ posthog.Logger = logger{} + +type logger struct{} + +func (logger) Debugf(format string, args ...any) { + slog.Debug(format, args...) +} + +func (logger) Logf(format string, args ...any) { + slog.Info(format, args...) +} + +func (logger) Warnf(format string, args ...any) { + slog.Warn(format, args...) +} + +func (logger) Errorf(format string, args ...any) { + slog.Error(format, args...) +} diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index ec48fc2956ac5ed3baa031ba2ed4b2f905b65ae0..74b1cb74659238de917c823872698f1b2ed31332 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -12,6 +12,7 @@ import ( "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/csync" + "github.com/charmbracelet/crush/internal/event" "github.com/charmbracelet/crush/internal/history" "github.com/charmbracelet/crush/internal/llm/prompt" "github.com/charmbracelet/crush/internal/llm/provider" @@ -25,12 +26,6 @@ import ( "github.com/charmbracelet/crush/internal/shell" ) -// Common errors -var ( - ErrRequestCancelled = errors.New("request canceled by user") - ErrSessionBusy = errors.New("session is currently processing another request") -) - type AgentEventType string const ( @@ -66,10 +61,11 @@ type Service interface { type agent struct { *pubsub.Broker[AgentEvent] - agentCfg config.Agent - sessions session.Service - messages message.Service - mcpTools []McpTool + agentCfg config.Agent + sessions session.Service + messages message.Service + permissions permission.Service + mcpTools []McpTool tools *csync.LazySlice[tools.BaseTool] // We need this to be able to update it when model changes @@ -237,6 +233,7 @@ func NewAgent( activeRequests: csync.NewMap[string, context.CancelFunc](), tools: csync.NewLazySlice(toolFn), promptQueue: csync.NewMap[string, []string](), + permissions: permissions, }, nil } @@ -365,8 +362,9 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac } genCtx, cancel := context.WithCancel(ctx) - a.activeRequests.Set(sessionID, cancel) + startTime := time.Now() + go func() { slog.Debug("Request started", "sessionID", sessionID) defer log.RecoverPanic("agent.Run", func() { @@ -377,16 +375,24 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content}) } result := a.processGeneration(genCtx, sessionID, content, attachmentParts) - if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) { - slog.Error(result.Error.Error()) + if result.Error != nil { + if isCancelledErr(result.Error) { + slog.Error("Request canceled", "sessionID", sessionID) + } else { + slog.Error("Request errored", "sessionID", sessionID, "error", result.Error.Error()) + event.Error(result.Error) + } + } else { + slog.Debug("Request completed", "sessionID", sessionID) } - slog.Debug("Request completed", "sessionID", sessionID) + a.eventPromptResponded(sessionID, time.Since(startTime).Truncate(time.Second)) a.activeRequests.Del(sessionID) cancel() a.Publish(pubsub.CreatedEvent, result) events <- result close(events) }() + a.eventPromptSent(sessionID) return events, nil } @@ -726,13 +732,13 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg if err := a.messages.Update(ctx, *assistantMsg); err != nil { return fmt.Errorf("failed to update message: %w", err) } - return a.TrackUsage(ctx, sessionID, a.Model(), event.Response.Usage) + return a.trackUsage(ctx, sessionID, a.Model(), event.Response.Usage) } return nil } -func (a *agent) TrackUsage(ctx context.Context, sessionID string, model catwalk.Model, usage provider.TokenUsage) error { +func (a *agent) trackUsage(ctx context.Context, sessionID string, model catwalk.Model, usage provider.TokenUsage) error { sess, err := a.sessions.Get(ctx, sessionID) if err != nil { return fmt.Errorf("failed to get session: %w", err) @@ -743,6 +749,8 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model catwalk. model.CostPer1MIn/1e6*float64(usage.InputTokens) + model.CostPer1MOut/1e6*float64(usage.OutputTokens) + a.eventTokensUsed(sessionID, usage, cost) + sess.Cost += cost sess.CompletionTokens = usage.OutputTokens + usage.CacheReadTokens sess.PromptTokens = usage.InputTokens + usage.CacheCreationTokens diff --git a/internal/llm/agent/errors.go b/internal/llm/agent/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..0e2f983d64b42b93ad3a51f32ce0335b0374a613 --- /dev/null +++ b/internal/llm/agent/errors.go @@ -0,0 +1,15 @@ +package agent + +import ( + "context" + "errors" +) + +var ( + ErrRequestCancelled = errors.New("request canceled by user") + ErrSessionBusy = errors.New("session is currently processing another request") +) + +func isCancelledErr(err error) bool { + return errors.Is(err, context.Canceled) || errors.Is(err, ErrRequestCancelled) +} diff --git a/internal/llm/agent/event.go b/internal/llm/agent/event.go new file mode 100644 index 0000000000000000000000000000000000000000..8642d9990dc31689292abe9f2b39e685462f158e --- /dev/null +++ b/internal/llm/agent/event.go @@ -0,0 +1,53 @@ +package agent + +import ( + "time" + + "github.com/charmbracelet/crush/internal/config" + "github.com/charmbracelet/crush/internal/event" + "github.com/charmbracelet/crush/internal/llm/provider" +) + +func (a *agent) eventPromptSent(sessionID string) { + event.PromptSent( + a.eventCommon(sessionID)..., + ) +} + +func (a *agent) eventPromptResponded(sessionID string, duration time.Duration) { + event.PromptResponded( + append( + a.eventCommon(sessionID), + "prompt duration pretty", duration.String(), + "prompt duration in seconds", int64(duration.Seconds()), + )..., + ) +} + +func (a *agent) eventTokensUsed(sessionID string, usage provider.TokenUsage, cost float64) { + event.TokensUsed( + append( + a.eventCommon(sessionID), + "input tokens", usage.InputTokens, + "output tokens", usage.OutputTokens, + "cache read tokens", usage.CacheReadTokens, + "cache creation tokens", usage.CacheCreationTokens, + "total tokens", usage.InputTokens+usage.OutputTokens+usage.CacheReadTokens+usage.CacheCreationTokens, + "cost", cost, + )..., + ) +} + +func (a *agent) eventCommon(sessionID string) []any { + cfg := config.Get() + currentModel := cfg.Models[cfg.Agents["coder"].Model] + + return []any{ + "session id", sessionID, + "provider", currentModel.Provider, + "model", currentModel.Model, + "reasoning effort", currentModel.ReasoningEffort, + "thinking mode", currentModel.Think, + "yolo mode", a.permissions.SkipRequests(), + } +} diff --git a/internal/log/log.go b/internal/log/log.go index bf99fe60fa9a5015029af171adfd6b3f9bf5596b..9463c3bd97956da3ab895b8600f79d1c05790844 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "time" + "github.com/charmbracelet/crush/internal/event" "gopkg.in/natefinch/lumberjack.v2" ) @@ -48,6 +49,8 @@ func Initialized() bool { func RecoverPanic(name string, cleanup func()) { if r := recover(); r != nil { + event.Error(r, "panic", true, "name", name) + // Create a timestamped panic log file timestamp := time.Now().Format("20060102-150405") filename := fmt.Sprintf("crush-panic-%s-%s.log", name, timestamp) diff --git a/internal/session/session.go b/internal/session/session.go index d988dac3414fa7dd00d13b375e1309f8d6c515dd..f83f66ffa4d1cfb75c6a0d41f09caebcb1c64cf3 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -5,6 +5,7 @@ import ( "database/sql" "github.com/charmbracelet/crush/internal/db" + "github.com/charmbracelet/crush/internal/event" "github.com/charmbracelet/crush/internal/pubsub" "github.com/google/uuid" ) @@ -48,6 +49,7 @@ func (s *service) Create(ctx context.Context, title string) (Session, error) { } session := s.fromDBItem(dbSession) s.Publish(pubsub.CreatedEvent, session) + event.SessionCreated() return session, nil } @@ -89,6 +91,7 @@ func (s *service) Delete(ctx context.Context, id string) error { return err } s.Publish(pubsub.DeletedEvent, session) + event.SessionDeleted() return nil } diff --git a/internal/tui/components/dialogs/sessions/sessions.go b/internal/tui/components/dialogs/sessions/sessions.go index 4e5cbdef7fdb42f4c667de7ac5bdd5066e7be4df..037eb5ebb727a24b8ab9bfda2e2c72943120e819 100644 --- a/internal/tui/components/dialogs/sessions/sessions.go +++ b/internal/tui/components/dialogs/sessions/sessions.go @@ -4,6 +4,7 @@ import ( "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/crush/internal/event" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/components/chat" "github.com/charmbracelet/crush/internal/tui/components/core" @@ -99,6 +100,7 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { selectedItem := s.sessionsList.SelectedItem() if selectedItem != nil { selected := *selectedItem + event.SessionSwitched() return s, tea.Sequence( util.CmdHandler(dialogs.CloseDialogMsg{}), util.CmdHandler( diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 0986aca31dcd779ca6fe611e1d71eff8ad6908e9..2c935810b833af01c582866ec38d5f7b277bc203 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -10,6 +10,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/app" "github.com/charmbracelet/crush/internal/config" + "github.com/charmbracelet/crush/internal/event" "github.com/charmbracelet/crush/internal/llm/agent" "github.com/charmbracelet/crush/internal/permission" "github.com/charmbracelet/crush/internal/pubsub" @@ -196,6 +197,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if a.app.CoderAgent.IsBusy() { return a, util.ReportWarn("Agent is busy, please wait...") } + config.Get().UpdatePreferredModel(msg.ModelType, msg.Model) // Update the agent with the new model/provider configuration @@ -211,6 +213,8 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // File Picker case commands.OpenFilePickerMsg: + event.FilePickerOpened() + if a.dialog.ActiveDialogID() == filepicker.FilePickerID { // If the commands dialog is already open, close it return a, util.CmdHandler(dialogs.CloseDialogMsg{}) diff --git a/main.go b/main.go index 072e3b35d2a2f408d8ed6a09423712b324df8b96..49dbcd7d3c045ae1510d7ca2055fa480c6fadadf 100644 --- a/main.go +++ b/main.go @@ -10,11 +10,13 @@ import ( _ "github.com/joho/godotenv/autoload" // automatically load .env files "github.com/charmbracelet/crush/internal/cmd" + "github.com/charmbracelet/crush/internal/event" "github.com/charmbracelet/crush/internal/log" ) func main() { defer log.RecoverPanic("main", func() { + event.Flush() slog.Error("Application terminated due to unhandled panic") }) diff --git a/schema.json b/schema.json index f0cb2053e188d918e4c49168080026de5f0bffe5..deb65846fe30ca689779e36745b9a429082c452b 100644 --- a/schema.json +++ b/schema.json @@ -320,6 +320,11 @@ "attribution": { "$ref": "#/$defs/Attribution", "description": "Attribution settings for generated content" + }, + "disable_metrics": { + "type": "boolean", + "description": "Disable sending metrics", + "default": false } }, "additionalProperties": false,