Detailed changes
@@ -1327,6 +1327,14 @@
"created_at": "2026-03-01T17:51:04Z",
"repoId": 987670088,
"pullRequestNo": 2337
+ },
+ {
+ "name": "ZeitbyteRepo",
+ "id": 187110998,
+ "comment_id": 4025287397,
+ "created_at": "2026-03-09T17:00:06Z",
+ "repoId": 987670088,
+ "pullRequestNo": 2390
}
]
}
@@ -67,7 +67,7 @@ jobs:
persist-credentials: false
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
- go-version: 1.26.0
+ go-version: 1.26.1
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck
@@ -1,39 +1,130 @@
# Crush Development Guide
+## Project Overview
+
+Crush is a terminal-based AI coding assistant built in Go by
+[Charm](https://charm.land). It connects to LLMs and gives them tools to read,
+write, and execute code. It supports multiple providers (Anthropic, OpenAI,
+Gemini, Bedrock, Copilot, Hyper, MiniMax, Vercel, and more), integrates with
+LSPs for code intelligence, and supports extensibility via MCP servers and
+agent skills.
+
+The module path is `github.com/charmbracelet/crush`.
+
+## Architecture
+
+```
+main.go CLI entry point (cobra via internal/cmd)
+internal/
+ app/app.go Top-level wiring: DB, config, agents, LSP, MCP, events
+ cmd/ CLI commands (root, run, login, models, stats, sessions)
+ config/
+ config.go Config struct, context file paths, agent definitions
+ load.go crush.json loading and validation
+ provider.go Provider configuration and model resolution
+ agent/
+ agent.go SessionAgent: runs LLM conversations per session
+ coordinator.go Coordinator: manages named agents ("coder", "task")
+ prompts.go Loads Go-template system prompts
+ templates/ System prompt templates (coder.md.tpl, task.md.tpl, etc.)
+ tools/ All built-in tools (bash, edit, view, grep, glob, etc.)
+ mcp/ MCP client integration
+ session/session.go Session CRUD backed by SQLite
+ message/ Message model and content types
+ db/ SQLite via sqlc, with migrations
+ sql/ Raw SQL queries (consumed by sqlc)
+ migrations/ Schema migrations
+ lsp/ LSP client manager, auto-discovery, on-demand startup
+ ui/ Bubble Tea v2 TUI (see internal/ui/AGENTS.md)
+ permission/ Tool permission checking and allow-lists
+ skills/ Skill file discovery and loading
+ shell/ Bash command execution with background job support
+ event/ Telemetry (PostHog)
+ pubsub/ Internal pub/sub for cross-component messaging
+ filetracker/ Tracks files touched per session
+ history/ Prompt history
+```
+
+### Key Dependency Roles
+
+- **`charm.land/fantasy`**: LLM provider abstraction layer. Handles protocol
+ differences between Anthropic, OpenAI, Gemini, etc. Used in `internal/app`
+ and `internal/agent`.
+- **`charm.land/bubbletea/v2`**: TUI framework powering the interactive UI.
+- **`charm.land/lipgloss/v2`**: Terminal styling.
+- **`charm.land/glamour/v2`**: Markdown rendering in the terminal.
+- **`charm.land/catwalk`**: Snapshot/golden-file testing for TUI components.
+- **`sqlc`**: Generates Go code from SQL queries in `internal/db/sql/`.
+
+### Key Patterns
+
+- **Config is a Service**: accessed via `config.Service`, not global state.
+- **Tools are self-documenting**: each tool has a `.go` implementation and a
+ `.md` description file in `internal/agent/tools/`.
+- **System prompts are Go templates**: `internal/agent/templates/*.md.tpl`
+ with runtime data injected.
+- **Context files**: Crush reads AGENTS.md, CRUSH.md, CLAUDE.md, GEMINI.md
+ (and `.local` variants) from the working directory for project-specific
+ instructions.
+- **Persistence**: SQLite + sqlc. All queries live in `internal/db/sql/`,
+ generated code in `internal/db/`. Migrations in `internal/db/migrations/`.
+- **Pub/sub**: `internal/pubsub` for decoupled communication between agent,
+ UI, and services.
+- **CGO disabled**: builds with `CGO_ENABLED=0` and
+ `GOEXPERIMENT=greenteagc`.
+
## Build/Test/Lint Commands
- **Build**: `go build .` or `go run .`
-- **Test**: `task test` or `go test ./...` (run single test: `go test ./internal/llm/prompt -run TestGetContextFromPaths`)
-- **Update Golden Files**: `go test ./... -update` (regenerates .golden files when test output changes)
- - Update specific package: `go test ./internal/tui/components/core -update` (in this case, we're updating "core")
+- **Test**: `task test` or `go test ./...` (run single test:
+ `go test ./internal/llm/prompt -run TestGetContextFromPaths`)
+- **Update Golden Files**: `go test ./... -update` (regenerates `.golden`
+ files when test output changes)
+ - Update specific package:
+ `go test ./internal/tui/components/core -update` (in this case,
+ we're updating "core")
- **Lint**: `task lint:fix`
- **Format**: `task fmt` (`gofumpt -w .`)
-- **Modernize**: `task modernize` (runs `modernize` which make code simplifications)
+- **Modernize**: `task modernize` (runs `modernize` which makes code
+ simplifications)
- **Dev**: `task dev` (runs with profiling enabled)
## Code Style Guidelines
-- **Imports**: Use `goimports` formatting, group stdlib, external, internal packages
-- **Formatting**: Use gofumpt (stricter than gofmt), enabled in golangci-lint
-- **Naming**: Standard Go conventions - PascalCase for exported, camelCase for unexported
-- **Types**: Prefer explicit types, use type aliases for clarity (e.g., `type AgentName string`)
-- **Error handling**: Return errors explicitly, use `fmt.Errorf` for wrapping
-- **Context**: Always pass `context.Context` as first parameter for operations
-- **Interfaces**: Define interfaces in consuming packages, keep them small and focused
-- **Structs**: Use struct embedding for composition, group related fields
-- **Constants**: Use typed constants with iota for enums, group in const blocks
-- **Testing**: Use testify's `require` package, parallel tests with `t.Parallel()`,
- `t.SetEnv()` to set environment variables. Always use `t.Tempdir()` when in
- need of a temporary directory. This directory does not need to be removed.
-- **JSON tags**: Use snake_case for JSON field names
-- **File permissions**: Use octal notation (0o755, 0o644) for file permissions
-- **Log messages**: Log messages must start with a capital letter (e.g., "Failed to save session" not "failed to save session")
- - This is enforced by `task lint:log` which runs as part of `task lint`
-- **Comments**: End comments in periods unless comments are at the end of the line.
+- **Imports**: Use `goimports` formatting, group stdlib, external, internal
+ packages.
+- **Formatting**: Use gofumpt (stricter than gofmt), enabled in
+ golangci-lint.
+- **Naming**: Standard Go conventions β PascalCase for exported, camelCase
+ for unexported.
+- **Types**: Prefer explicit types, use type aliases for clarity (e.g.,
+ `type AgentName string`).
+- **Error handling**: Return errors explicitly, use `fmt.Errorf` for
+ wrapping.
+- **Context**: Always pass `context.Context` as first parameter for
+ operations.
+- **Interfaces**: Define interfaces in consuming packages, keep them small
+ and focused.
+- **Structs**: Use struct embedding for composition, group related fields.
+- **Constants**: Use typed constants with iota for enums, group in const
+ blocks.
+- **Testing**: Use testify's `require` package, parallel tests with
+ `t.Parallel()`, `t.SetEnv()` to set environment variables. Always use
+ `t.Tempdir()` when in need of a temporary directory. This directory does
+ not need to be removed.
+- **JSON tags**: Use snake_case for JSON field names.
+- **File permissions**: Use octal notation (0o755, 0o644) for file
+ permissions.
+- **Log messages**: Log messages must start with a capital letter (e.g.,
+ "Failed to save session" not "failed to save session").
+ - This is enforced by `task lint:log` which runs as part of `task lint`.
+- **Comments**: End comments in periods unless comments are at the end of the
+ line.
## Testing with Mock Providers
-When writing tests that involve provider configurations, use the mock providers to avoid API calls:
+When writing tests that involve provider configurations, use the mock
+providers to avoid API calls:
```go
func TestYourFunction(t *testing.T) {
@@ -70,9 +161,12 @@ func TestYourFunction(t *testing.T) {
## Committing
-- ALWAYS use semantic commits (`fix:`, `feat:`, `chore:`, `refactor:`, `docs:`, `sec:`, etc).
+- ALWAYS use semantic commits (`fix:`, `feat:`, `chore:`, `refactor:`,
+ `docs:`, `sec:`, etc).
- Try to keep commits to one line, not including your attribution. Only use
multi-line commits when additional context is truly necessary.
## Working on the TUI (UI)
-Anytime you need to work on the tui before starting work read the internal/ui/AGENTS.md file
+
+Anytime you need to work on the TUI, read `internal/ui/AGENTS.md` before
+starting work.
@@ -424,6 +424,25 @@ git clone https://github.com/anthropics/skills.git _temp
mv _temp/skills/* . ; rm -r -force _temp
```
+### Desktop notifications
+
+Crush sends desktop notifications when a tool call requires permission and when
+the agent finishes its turn. They're only sent when the terminal window isn't
+focused _and_ your terminal supports reporting the focus state.
+
+```jsonc
+{
+ "$schema": "https://charm.land/crush.json",
+ "options": {
+ "disable_notifications": false // default
+ }
+}
+```
+
+To disable desktop notifications, set `disable_notifications` to `true` in your
+configuration. On macOS, notifications currently lack icons due to platform
+limitations.
+
### Initialization
When you initialize a project, Crush analyzes your codebase and creates
@@ -1,15 +1,15 @@
module github.com/charmbracelet/crush
-go 1.26.0
+go 1.26.1
require (
charm.land/bubbles/v2 v2.0.0
- charm.land/bubbletea/v2 v2.0.0
- charm.land/catwalk v0.25.3
- charm.land/fantasy v0.11.0
- charm.land/glamour/v2 v2.0.0-20260123212943-6014aa153a9b
- charm.land/lipgloss/v2 v2.0.0
- charm.land/log/v2 v2.0.0-20251110204020-529bb77f35da
+ charm.land/bubbletea/v2 v2.0.2
+ charm.land/catwalk v0.28.4
+ charm.land/fantasy v0.12.1
+ charm.land/glamour/v2 v2.0.0
+ charm.land/lipgloss/v2 v2.0.1
+ charm.land/log/v2 v2.0.0
charm.land/x/vcr v0.1.1
github.com/JohannesKaufmann/html-to-markdown v1.6.0
github.com/MakeNowJust/heredoc v1.0.0
@@ -17,11 +17,11 @@ require (
github.com/alecthomas/chroma/v2 v2.23.1
github.com/atotto/clipboard v0.1.4
github.com/aymanbagabas/go-nativeclipboard v0.1.3
- github.com/aymanbagabas/go-udiff v0.4.0
+ github.com/aymanbagabas/go-udiff v0.4.1
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/charlievieth/fastwalk v1.0.14
- github.com/charmbracelet/colorprofile v0.4.2
- github.com/charmbracelet/fang v0.4.4
+ github.com/charmbracelet/colorprofile v0.4.3
+ github.com/charmbracelet/fang v1.0.0
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/editor v0.2.0
@@ -38,6 +38,7 @@ require (
github.com/denisbrodbeck/machineid v1.0.1
github.com/disintegration/imaging v1.6.2
github.com/dustin/go-humanize v1.0.1
+ github.com/gen2brain/beeep v0.11.2
github.com/go-git/go-git/v5 v5.17.0
github.com/google/uuid v1.6.0
github.com/invopop/jsonschema v0.13.0
@@ -46,7 +47,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20
github.com/modelcontextprotocol/go-sdk v1.4.0
- github.com/ncruces/go-sqlite3 v0.30.5
+ github.com/ncruces/go-sqlite3 v0.31.1
github.com/nxadm/tail v1.4.11
github.com/openai/openai-go/v2 v2.7.1
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
@@ -63,13 +64,13 @@ require (
github.com/zeebo/xxh3 v1.1.0
go.uber.org/goleak v1.3.0
golang.org/x/net v0.51.0
- golang.org/x/sync v0.19.0
+ golang.org/x/sync v0.20.0
golang.org/x/text v0.34.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.46.1
mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5
- mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
+ mvdan.cc/sh/v3 v3.13.0
)
require (
@@ -77,24 +78,25 @@ require (
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
+ git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
- github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect
- github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
- github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect
- github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
- github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
- github.com/aws/smithy-go v1.24.1 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
+ github.com/aws/smithy-go v1.24.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
@@ -105,18 +107,21 @@ require (
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
- github.com/ebitengine/purego v0.10.0-alpha.5 // indirect
+ github.com/ebitengine/purego v0.10.0 // indirect
+ github.com/esiqveland/notify v0.13.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.8.0 // indirect
- github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
+ github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
+ github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
@@ -127,10 +132,11 @@ require (
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/jackmordaunt/icns/v3 v3.0.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
- github.com/kaptinlin/go-i18n v0.2.11 // indirect
- github.com/kaptinlin/jsonpointer v0.4.16 // indirect
- github.com/kaptinlin/jsonschema v0.7.3 // indirect
+ github.com/kaptinlin/go-i18n v0.2.12 // indirect
+ github.com/kaptinlin/jsonpointer v0.4.17 // indirect
+ github.com/kaptinlin/jsonschema v0.7.5 // indirect
github.com/kaptinlin/messageformat-go v0.4.18 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
@@ -147,13 +153,17 @@ require (
github.com/muesli/roff v0.1.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.3 // indirect
+ github.com/sergeymakinen/go-bmp v1.0.0 // indirect
+ github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
+ github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
@@ -175,13 +185,13 @@ require (
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/image v0.36.0 // indirect
- golang.org/x/oauth2 v0.35.0 // indirect
- golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/oauth2 v0.36.0 // indirect
+ golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
- google.golang.org/api v0.267.0 // indirect
- google.golang.org/genai v1.48.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
+ google.golang.org/api v0.269.0 // indirect
+ google.golang.org/genai v1.49.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect
@@ -1,17 +1,17 @@
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
-charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
-charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
-charm.land/catwalk v0.25.3 h1:mkeICGwUPR9ZeOKNaeRmUrTyDJazTFYiNFWSeyjhM1A=
-charm.land/catwalk v0.25.3/go.mod h1:rFC/V96rIHX7VES215c/qzI1EW/Moo1ggs1Q6seTy5s=
-charm.land/fantasy v0.11.0 h1:KrYa7B3JMCViXsbDyho9vLdzoml9Id8OgyytowrmkNY=
-charm.land/fantasy v0.11.0/go.mod h1:NtQpqji9blpicYopEzcbgj8mIR4fOMjwK0wyr/D9D5M=
-charm.land/glamour/v2 v2.0.0-20260123212943-6014aa153a9b h1:A6IUUyChZDWP16RUdRJCfmYISAKWQGyIcfhZJUCViQ0=
-charm.land/glamour/v2 v2.0.0-20260123212943-6014aa153a9b/go.mod h1:J3kVhY6oHXZq5f+8vC3hmDO95fEvbqj3z7xDwxrfzU8=
-charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
-charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
-charm.land/log/v2 v2.0.0-20251110204020-529bb77f35da h1:vZa/Ow0uLclpfaDY0ubjzE+B0eLQqi2zanmpeALanow=
-charm.land/log/v2 v2.0.0-20251110204020-529bb77f35da/go.mod h1:Tj12StbPc4GwksDF6XwhC9wdXouinIVxRGKKmmmzdSU=
+charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
+charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
+charm.land/catwalk v0.28.4 h1:YaaXA1k0v7CKvvT+Gh1pDD7XrlUR93kROdaWqkkglRw=
+charm.land/catwalk v0.28.4/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg=
+charm.land/fantasy v0.12.1 h1:awszoi5O9FIjMEkfyCMiLJfVRNLckp/zQkFrA6IxQqc=
+charm.land/fantasy v0.12.1/go.mod h1:QeRVUeG1XNTWBszRAbhUtPyX1VWs6zjkCxwfcwnICdc=
+charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
+charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=
+charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
+charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
+charm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s=
+charm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0=
charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q=
charm.land/x/vcr v0.1.1/go.mod h1:eByq2gqzWvcct/8XE2XO5KznoWEBiXH56+y2gphbltM=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
@@ -22,6 +22,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
+git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
+git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
@@ -48,40 +50,40 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
-github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
-github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
-github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
-github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
-github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
-github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
+github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
+github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
+github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
+github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
+github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
+github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-nativeclipboard v0.1.3 h1:FmAWHPTwneAixu7uGDn3cL42xPlUCdNp2J8egMn3P1k=
github.com/aymanbagabas/go-nativeclipboard v0.1.3/go.mod h1:2o7MyZwwi4pmXXpOpvOS5FwaHyoCIUks0ktjUvB0EoE=
-github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
-github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
+github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
+github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -96,10 +98,10 @@ github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICg
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab h1:J7XQLgl9sefgTnTGrmX3xqvp5o6MCiBzEjGv5igAlc4=
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8=
-github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
-github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
-github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY=
-github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
+github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
+github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
+github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ=
+github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
@@ -151,26 +153,30 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/ebitengine/purego v0.10.0-alpha.5 h1:IUIZ1pu0wnpxrn7o6utj8AeoZBS2upI11kLcddBF414=
-github.com/ebitengine/purego v0.10.0-alpha.5/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
+github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
+github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=
+github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWDA=
+github.com/gen2brain/beeep v0.11.2/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
-github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
-github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
+github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
+github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -178,6 +184,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
@@ -186,6 +194,9 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
+github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
@@ -220,6 +231,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
+github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
+github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -227,12 +240,12 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d h1:on25kP+Sx7sxUMRQiA8gdcToAGet4DK/EIA30mXre+4=
github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d/go.mod h1:SV0W0APWP9MZ1/gfDQ/NzzTlWdIgYZ/ZbpN4d/UXRYw=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/kaptinlin/go-i18n v0.2.11 h1:OayNt8mWt8nDaqAOp09/C1VG9Y5u8LpQnnxbyGARDV4=
-github.com/kaptinlin/go-i18n v0.2.11/go.mod h1:pVcu9qsW5pOIOoZFJXesRYmLos1vMQrby70JPAoWmJU=
-github.com/kaptinlin/jsonpointer v0.4.16 h1:Ux4w4FY+uLv+K+TxaCJtM/TpPv+1+eS6gH4Z9/uhOuA=
-github.com/kaptinlin/jsonpointer v0.4.16/go.mod h1:SsfsjqnHG5zuKo1DTBzk1VknaHlL4osHw+X9kZKukpU=
-github.com/kaptinlin/jsonschema v0.7.3 h1:kyIydij76ORiSxmfy0xFYy0cOx8MwG6pyyaSoQshsK4=
-github.com/kaptinlin/jsonschema v0.7.3/go.mod h1:Ys6zr+W6/1330FzZEouFrAYImK+AmYt5HQVTHQQXQo8=
+github.com/kaptinlin/go-i18n v0.2.12 h1:ywDsvb4KDFddMC2dpI/rrIzGU2mWUSvHmWUm9BMsdl4=
+github.com/kaptinlin/go-i18n v0.2.12/go.mod h1:pVcu9qsW5pOIOoZFJXesRYmLos1vMQrby70JPAoWmJU=
+github.com/kaptinlin/jsonpointer v0.4.17 h1:mY9k8ciWncxbsECyaxKnR0MdmxamNdp2tLQkAKVrtSk=
+github.com/kaptinlin/jsonpointer v0.4.17/go.mod h1:SsfsjqnHG5zuKo1DTBzk1VknaHlL4osHw+X9kZKukpU=
+github.com/kaptinlin/jsonschema v0.7.5 h1:jkK4a3NyzNoGlvu12CsL3IcqNMVa5sL51HPVa0nWcPY=
+github.com/kaptinlin/jsonschema v0.7.5/go.mod h1:3gIWnptl+SWMyfMR2r4TXXd0xsQZ1m50AKrwmcUONSg=
github.com/kaptinlin/messageformat-go v0.4.18 h1:RBlHVWgZyoxTcUgGWBsl2AcyScq/urqbLZvzgryTmSI=
github.com/kaptinlin/messageformat-go v0.4.18/go.mod h1:ntI3154RnqJgr7GaC+vZBnIExl2V3sv9selvRNNEM24=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
@@ -276,12 +289,14 @@ github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
-github.com/ncruces/go-sqlite3 v0.30.5 h1:6usmTQ6khriL8oWilkAZSJM/AIpAlVL2zFrlcpDldCE=
-github.com/ncruces/go-sqlite3 v0.30.5/go.mod h1:0I0JFflTKzfs3Ogfv8erP7CCoV/Z8uxigVDNOR0AQ5E=
+github.com/ncruces/go-sqlite3 v0.31.1 h1:F76NF4NTLNOabLUKuEb2xqjBW+/Ub+MR59/Q7dACRo8=
+github.com/ncruces/go-sqlite3 v0.31.1/go.mod h1:L9OWFjYG/+4dq9O6bFCYoWLG0a7LmtgR6v26TvABmwg=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8=
@@ -321,6 +336,10 @@ github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
+github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
+github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
+github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
+github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
@@ -334,10 +353,17 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
+github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -427,8 +453,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
-golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
-golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -436,8 +462,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -454,8 +480,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -493,12 +519,12 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
-google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
-google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
-google.golang.org/genai v1.48.0 h1:1vb15G291wAjJJueisMDpUhssljhEdJU2t5qTidrVPs=
-google.golang.org/genai v1.48.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg=
+google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE=
+google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0=
+google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@@ -518,6 +544,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
@@ -550,5 +577,5 @@ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5 h1:mO2lyKtGwu4mGQ+Qqjx0+fd5UU5BXhX/rslFmxd5aco=
mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5/go.mod h1:Of9PCedbLDYT8b3EyiYG64rNnx5nOp27OLCVdDrjJyo=
-mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5 h1:e7Z/Lgw/zMijvQBVrfh/vUDZ+9FpuSLrJDVGBuoJtuo=
-mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5/go.mod h1:P21wo2gLLe3426sP+CmANLBaixSEbRtPl35w3YlM6dg=
+mvdan.cc/sh/v3 v3.13.0 h1:dSfq/MVsY4w0Vsi6Lbs0IcQquMVqLdKLESAOZjuHdLg=
+mvdan.cc/sh/v3 v3.13.0/go.mod h1:KV1GByGPc/Ho0X1E6Uz9euhsIQEj4hwyKnodLlFLoDM=
@@ -32,19 +32,22 @@ import (
"charm.land/fantasy/providers/vercel"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/crush/internal/agent/hyper"
+ "github.com/charmbracelet/crush/internal/agent/notify"
"github.com/charmbracelet/crush/internal/agent/tools"
"github.com/charmbracelet/crush/internal/agent/tools/mcp"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/permission"
+ "github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/session"
"github.com/charmbracelet/crush/internal/stringext"
+ "github.com/charmbracelet/crush/internal/version"
"github.com/charmbracelet/x/exp/charmtone"
)
const (
- defaultSessionName = "Untitled Session"
+ DefaultSessionName = "Untitled Session"
// Constants for auto-summarization thresholds
largeContextWindowThreshold = 200_000
@@ -72,6 +75,7 @@ type SessionAgentCall struct {
TopK *int64
FrequencyPenalty *float64
PresencePenalty *float64
+ NonInteractive bool
}
type SessionAgent interface {
@@ -108,6 +112,7 @@ type sessionAgent struct {
messages message.Service
disableAutoSummarize bool
isYolo bool
+ notify pubsub.Publisher[notify.Notification]
messageQueue *csync.Map[string, []SessionAgentCall]
activeRequests *csync.Map[string, context.CancelFunc]
@@ -124,6 +129,7 @@ type SessionAgentOptions struct {
Sessions session.Service
Messages message.Service
Tools []fantasy.AgentTool
+ Notify pubsub.Publisher[notify.Notification]
}
func NewSessionAgent(
@@ -140,6 +146,7 @@ func NewSessionAgent(
disableAutoSummarize: opts.DisableAutoSummarize,
tools: csync.NewSliceFrom(opts.Tools),
isYolo: opts.IsYolo,
+ notify: opts.Notify,
messageQueue: csync.NewMap[string, []SessionAgentCall](),
activeRequests: csync.NewMap[string, context.CancelFunc](),
}
@@ -194,6 +201,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
largeModel.Model,
fantasy.WithSystemPrompt(systemPrompt),
fantasy.WithTools(agentTools...),
+ fantasy.WithUserAgent("Charm Crush/"+version.Version),
)
sessionLock := sync.Mutex{}
@@ -533,6 +541,16 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
return nil, err
}
+ // Send notification that agent has finished its turn (skip for
+ // nested/non-interactive sessions).
+ if !call.NonInteractive && a.notify != nil {
+ a.notify.Publish(pubsub.CreatedEvent, notify.Notification{
+ SessionID: call.SessionID,
+ SessionTitle: currentSession.Title,
+ Type: notify.TypeAgentFinished,
+ })
+ }
+
if shouldSummarize {
a.activeRequests.Del(call.SessionID)
if summarizeErr := a.Summarize(genCtx, call.SessionID, call.ProviderOptions); summarizeErr != nil {
@@ -595,6 +613,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
agent := fantasy.NewAgent(largeModel.Model,
fantasy.WithSystemPrompt(string(summaryPrompt)),
+ fantasy.WithUserAgent("Charm Crush/"+version.Version),
)
summaryMessage, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
Role: message.Assistant,
@@ -790,6 +809,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
return fantasy.NewAgent(m,
fantasy.WithSystemPrompt(string(p)+"\n /no_think"),
fantasy.WithMaxOutputTokens(tok),
+ fantasy.WithUserAgent("Charm Crush/"+version.Version),
)
}
@@ -825,7 +845,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
// Welp, the large model didn't work either. Use the default
// session name and return.
slog.Error("Error generating title with large model", "err", err)
- saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, defaultSessionName, 0, 0, 0)
+ saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, DefaultSessionName, 0, 0, 0)
if saveErr != nil {
slog.Error("Failed to save session title and usage", "error", saveErr)
}
@@ -837,7 +857,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
// Actually, we didn't get a response so we can't. Use the default
// session name and return.
slog.Error("Response is nil; can't generate title")
- saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, defaultSessionName, 0, 0, 0)
+ saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, DefaultSessionName, 0, 0, 0)
if saveErr != nil {
slog.Error("Failed to save session title and usage", "error", saveErr)
}
@@ -852,7 +872,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
title = thinkTagRegex.ReplaceAllString(title, "")
title = strings.TrimSpace(title)
- title = cmp.Or(title, defaultSessionName)
+ title = cmp.Or(title, DefaultSessionName)
// Calculate usage and cost.
var openrouterCost *float64
@@ -153,7 +153,15 @@ func testSessionAgent(env fakeEnv, large, small fantasy.LanguageModel, systemPro
DefaultMaxTokens: 10000,
},
}
- agent := NewSessionAgent(SessionAgentOptions{largeModel, smallModel, "", systemPrompt, false, false, true, env.sessions, env.messages, tools})
+ agent := NewSessionAgent(SessionAgentOptions{
+ LargeModel: largeModel,
+ SmallModel: smallModel,
+ SystemPrompt: systemPrompt,
+ IsYolo: true,
+ Sessions: env.sessions,
+ Messages: env.messages,
+ Tools: tools,
+ })
return agent
}
@@ -18,6 +18,7 @@ import (
"charm.land/catwalk/pkg/catwalk"
"charm.land/fantasy"
"github.com/charmbracelet/crush/internal/agent/hyper"
+ "github.com/charmbracelet/crush/internal/agent/notify"
"github.com/charmbracelet/crush/internal/agent/prompt"
"github.com/charmbracelet/crush/internal/agent/tools"
"github.com/charmbracelet/crush/internal/config"
@@ -28,6 +29,7 @@ import (
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/oauth/copilot"
"github.com/charmbracelet/crush/internal/permission"
+ "github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/session"
"golang.org/x/sync/errgroup"
@@ -43,6 +45,18 @@ import (
"github.com/qjebbs/go-jsons"
)
+// Coordinator errors.
+var (
+ errCoderAgentNotConfigured = errors.New("coder agent not configured")
+ errModelProviderNotConfigured = errors.New("model provider not configured")
+ errLargeModelNotSelected = errors.New("large model not selected")
+ errSmallModelNotSelected = errors.New("small model not selected")
+ errLargeModelProviderNotConfigured = errors.New("large model provider not configured")
+ errSmallModelProviderNotConfigured = errors.New("small model provider not configured")
+ errLargeModelNotFound = errors.New("large model not found in provider config")
+ errSmallModelNotFound = errors.New("small model not found in provider config")
+)
+
type Coordinator interface {
// INFO: (kujtim) this is not used yet we will use this when we have multiple agents
// SetMainAgent(string)
@@ -68,6 +82,7 @@ type coordinator struct {
history history.Service
filetracker filetracker.Service
lspManager *lsp.Manager
+ notify pubsub.Publisher[notify.Notification]
currentAgent SessionAgent
agents map[string]SessionAgent
@@ -84,6 +99,7 @@ func NewCoordinator(
history history.Service,
filetracker filetracker.Service,
lspManager *lsp.Manager,
+ notify pubsub.Publisher[notify.Notification],
) (Coordinator, error) {
c := &coordinator{
cfg: cfg,
@@ -93,12 +109,13 @@ func NewCoordinator(
history: history,
filetracker: filetracker,
lspManager: lspManager,
+ notify: notify,
agents: make(map[string]SessionAgent),
}
agentCfg, ok := cfg.Agents[config.AgentCoder]
if !ok {
- return nil, errors.New("coder agent not configured")
+ return nil, errCoderAgentNotConfigured
}
// TODO: make this dynamic when we support multiple agents
@@ -146,7 +163,7 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
if !ok {
- return nil, errors.New("model provider not configured")
+ return nil, errModelProviderNotConfigured
}
mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
@@ -369,16 +386,17 @@ func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, age
largeProviderCfg, _ := c.cfg.Providers.Get(large.ModelCfg.Provider)
result := NewSessionAgent(SessionAgentOptions{
- large,
- small,
- largeProviderCfg.SystemPromptPrefix,
- "",
- isSubAgent,
- c.cfg.Options.DisableAutoSummarize,
- c.permissions.SkipRequests(),
- c.sessions,
- c.messages,
- nil,
+ LargeModel: large,
+ SmallModel: small,
+ SystemPromptPrefix: largeProviderCfg.SystemPromptPrefix,
+ SystemPrompt: "",
+ IsSubAgent: isSubAgent,
+ DisableAutoSummarize: c.cfg.Options.DisableAutoSummarize,
+ IsYolo: c.permissions.SkipRequests(),
+ Sessions: c.sessions,
+ Messages: c.messages,
+ Tools: nil,
+ Notify: c.notify,
})
c.readyWg.Go(func() error {
@@ -498,16 +516,16 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
func (c *coordinator) buildAgentModels(ctx context.Context, isSubAgent bool) (Model, Model, error) {
largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
if !ok {
- return Model{}, Model{}, errors.New("large model not selected")
+ return Model{}, Model{}, errLargeModelNotSelected
}
smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
if !ok {
- return Model{}, Model{}, errors.New("small model not selected")
+ return Model{}, Model{}, errSmallModelNotSelected
}
largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
if !ok {
- return Model{}, Model{}, errors.New("large model provider not configured")
+ return Model{}, Model{}, errLargeModelProviderNotConfigured
}
largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg, isSubAgent)
@@ -517,7 +535,7 @@ func (c *coordinator) buildAgentModels(ctx context.Context, isSubAgent bool) (Mo
smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
if !ok {
- return Model{}, Model{}, errors.New("small model provider not configured")
+ return Model{}, Model{}, errSmallModelProviderNotConfigured
}
smallProvider, err := c.buildProvider(smallProviderCfg, smallModelCfg, true)
@@ -540,11 +558,11 @@ func (c *coordinator) buildAgentModels(ctx context.Context, isSubAgent bool) (Mo
}
if largeCatwalkModel == nil {
- return Model{}, Model{}, errors.New("large model not found in provider config")
+ return Model{}, Model{}, errLargeModelNotFound
}
if smallCatwalkModel == nil {
- return Model{}, Model{}, errors.New("small model not found in provider config")
+ return Model{}, Model{}, errSmallModelNotFound
}
largeModelID := largeModelCfg.Model
@@ -773,19 +791,8 @@ func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
if model.Think {
return true
}
-
- if model.ProviderOptions == nil {
- return false
- }
-
opts, err := anthropic.ParseOptions(model.ProviderOptions)
- if err != nil {
- return false
- }
- if opts.Thinking != nil {
- return true
- }
- return false
+ return err == nil && opts.Thinking != nil
}
func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel, isSubAgent bool) (fantasy.Provider, error) {
@@ -883,7 +890,7 @@ func (c *coordinator) UpdateModels(ctx context.Context) error {
agentCfg, ok := c.cfg.Agents[config.AgentCoder]
if !ok {
- return errors.New("coder agent not configured")
+ return errCoderAgentNotConfigured
}
tools, err := c.buildTools(ctx, agentCfg)
@@ -920,7 +927,7 @@ func (c *coordinator) QueuedPromptsList(sessionID string) []string {
func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
if !ok {
- return errors.New("model provider not configured")
+ return errModelProviderNotConfigured
}
return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
}
@@ -995,7 +1002,7 @@ func (c *coordinator) runSubAgent(ctx context.Context, params subAgentParams) (f
providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
if !ok {
- return fantasy.ToolResponse{}, errors.New("model provider not configured")
+ return fantasy.ToolResponse{}, errModelProviderNotConfigured
}
// Run the agent
@@ -1009,6 +1016,7 @@ func (c *coordinator) runSubAgent(ctx context.Context, params subAgentParams) (f
TopK: model.ModelCfg.TopK,
FrequencyPenalty: model.ModelCfg.FrequencyPenalty,
PresencePenalty: model.ModelCfg.PresencePenalty,
+ NonInteractive: true,
})
if err != nil {
return fantasy.NewTextErrorResponse("error generating response"), nil
@@ -0,0 +1,19 @@
+// Package notify defines domain notification types for agent events.
+// These types are decoupled from UI concerns so the agent can publish
+// events without importing UI packages.
+package notify
+
+// Type identifies the kind of agent notification.
+type Type string
+
+const (
+ // TypeAgentFinished indicates the agent has completed its turn.
+ TypeAgentFinished Type = "agent_finished"
+)
+
+// Notification represents a domain event published by the agent.
+type Notification struct {
+ SessionID string
+ SessionTitle string
+ Type Type
+}
@@ -19,6 +19,7 @@ import (
"charm.land/fantasy"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/crush/internal/agent"
+ "github.com/charmbracelet/crush/internal/agent/notify"
"github.com/charmbracelet/crush/internal/agent/tools/mcp"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/db"
@@ -68,8 +69,9 @@ type App struct {
tuiWG *sync.WaitGroup
// global context and cleanup functions
- globalCtx context.Context
- cleanupFuncs []func(context.Context) error
+ globalCtx context.Context
+ cleanupFuncs []func(context.Context) error
+ agentNotifications *pubsub.Broker[notify.Notification]
}
// New initializes a new application instance.
@@ -96,9 +98,10 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
config: cfg,
- events: make(chan tea.Msg, 100),
- serviceEventsWG: &sync.WaitGroup{},
- tuiWG: &sync.WaitGroup{},
+ events: make(chan tea.Msg, 100),
+ serviceEventsWG: &sync.WaitGroup{},
+ tuiWG: &sync.WaitGroup{},
+ agentNotifications: pubsub.NewBroker[notify.Notification](),
}
app.setupEvents()
@@ -112,7 +115,7 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
app.cleanupFuncs = append(
app.cleanupFuncs,
func(context.Context) error { return conn.Close() },
- mcp.Close,
+ func(ctx context.Context) error { return mcp.Close(ctx) },
)
// TODO: remove the concept of agent config, most likely.
@@ -143,6 +146,11 @@ func (app *App) Config() *config.Config {
return app.config
}
+// AgentNotifications returns the broker for agent notification events.
+func (app *App) AgentNotifications() *pubsub.Broker[notify.Notification] {
+ return app.agentNotifications
+}
+
// RunNonInteractive runs the application in non-interactive mode with the
// given prompt, printing to stdout.
func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool) error {
@@ -213,18 +221,7 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
defer stopSpinner()
- const maxPromptLengthForTitle = 100
- const titlePrefix = "Non-interactive: "
- var titleSuffix string
-
- if len(prompt) > maxPromptLengthForTitle {
- titleSuffix = prompt[:maxPromptLengthForTitle] + "..."
- } else {
- titleSuffix = prompt
- }
- title := titlePrefix + titleSuffix
-
- sess, err := app.Sessions.Create(ctx, title)
+ sess, err := app.Sessions.Create(ctx, agent.DefaultSessionName)
if err != nil {
return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
}
@@ -425,6 +422,7 @@ func (app *App) setupEvents() {
setupSubscriber(ctx, app.serviceEventsWG, "permissions", app.Permissions.Subscribe, app.events)
setupSubscriber(ctx, app.serviceEventsWG, "permissions-notifications", app.Permissions.SubscribeNotifications, app.events)
setupSubscriber(ctx, app.serviceEventsWG, "history", app.History.Subscribe, app.events)
+ setupSubscriber(ctx, app.serviceEventsWG, "agent-notifications", app.agentNotifications.Subscribe, app.events)
setupSubscriber(ctx, app.serviceEventsWG, "mcp", mcp.SubscribeEvents, app.events)
setupSubscriber(ctx, app.serviceEventsWG, "lsp", SubscribeLSPEvents, app.events)
cleanupFunc := func(context.Context) error {
@@ -497,6 +495,7 @@ func (app *App) InitCoderAgent(ctx context.Context) error {
app.History,
app.FileTracker,
app.LSPManager,
+ app.agentNotifications,
)
if err != nil {
slog.Error("Failed to create coder agent", "err", err)
@@ -52,29 +52,26 @@ func init() {
var rootCmd = &cobra.Command{
Use: "crush",
- Short: "An AI assistant for software development",
- Long: "An AI assistant for software development and similar tasks with direct access to the terminal",
+ Short: "A terminal-first AI assistant for software development",
+ Long: "A glamorous, terminal-first AI assistant for software development and adjacent tasks",
Example: `
# Run in interactive mode
crush
-# Run with debug logging
-crush -d
+# Run non-interactively
+crush run "Guess my 5 favorite PokΓ©mon"
-# Run with debug logging in a specific directory
-crush -d -c /path/to/project
-
-# Run with custom data directory
-crush -D /path/to/custom/.crush
+# Run a non-interactively with pipes and redirection
+cat README.md | crush run "make this more glamorous" > GLAMOROUS_README.md
-# Print version
-crush -v
+# Run with debug logging in a specific directory
+crush --debug --cwd /path/to/project
-# Run a single non-interactive prompt
-crush run "Explain the use of context in Go"
+# Run in yolo mode (auto-accept all permissions; use with care)
+crush --yolo
-# Run in dangerous mode (auto-accept all permissions)
-crush -y
+# Run with custom data directory
+crush --data-dir /path/to/custom/.crush
`,
RunE: func(cmd *cobra.Command, args []string) error {
app, err := setupAppWithProgressBar(cmd)
@@ -20,7 +20,7 @@ var runCmd = &cobra.Command{
The prompt can be provided as arguments or piped from stdin.`,
Example: `
# Run a simple prompt
-crush run Explain the use of context in Go
+crush run "Guess my 5 favorite PokΓ©mon"
# Pipe input from stdin
curl https://charm.land | crush run "Summarize this website"
@@ -28,10 +28,13 @@ curl https://charm.land | crush run "Summarize this website"
# Read from a file
crush run "What is this code doing?" <<< prrr.go
+# Redirect output to a file
+crush run "Generate a hot README for this project" > MY_HOT_README.md
+
# Run in quiet mode (hide the spinner)
crush run --quiet "Generate a README for this project"
-# Run in verbose mode
+# Run in verbose mode (show logs)
crush run --verbose "Generate a README for this project"
`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -75,9 +78,6 @@ crush run --verbose "Generate a README for this project"
return app.RunNonInteractive(ctx, os.Stdout, prompt, largeModel, smallModel, quiet || verbose)
},
- PostRun: func(cmd *cobra.Command, args []string) {
- event.AppExited()
- },
}
func init() {
@@ -261,6 +261,7 @@ type Options struct {
InitializeAs string `json:"initialize_as,omitempty" jsonschema:"description=Name of the context file to create/update during project initialization,default=AGENTS.md,example=AGENTS.md,example=CRUSH.md,example=CLAUDE.md,example=docs/LLMs.md"`
AutoLSP *bool `json:"auto_lsp,omitempty" jsonschema:"description=Automatically setup LSPs based on root markers,default=true"`
Progress *bool `json:"progress,omitempty" jsonschema:"description=Show indeterminate progress updates during long operations,default=true"`
+ DisableNotifications bool `json:"disable_notifications,omitempty" jsonschema:"description=Disable desktop notifications,default=false"`
}
type MCPs map[string]MCPConfig
@@ -84,7 +84,7 @@ func send(event string, props ...any) {
// Error logs an error event to PostHog with the error type and message.
func Error(errToLog any, props ...any) {
- if client == nil || errToLog == nil {
+ if client == nil || distinctId == "" || errToLog == nil {
return
}
posthogErr := client.Enqueue(posthog.NewDefaultException(
@@ -134,6 +134,12 @@ func (s *permissionService) Request(ctx context.Context, opts CreatePermissionRe
return true, nil
}
+ // Check if the tool/action combination is in the allowlist
+ commandKey := opts.ToolName + ":" + opts.Action
+ if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
+ return true, nil
+ }
+
// tell the UI that a permission was requested
s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
ToolCallID: opts.ToolCallID,
@@ -141,12 +147,6 @@ func (s *permissionService) Request(ctx context.Context, opts CreatePermissionRe
s.requestMu.Lock()
defer s.requestMu.Unlock()
- // Check if the tool/action combination is in the allowlist
- commandKey := opts.ToolName + ":" + opts.Action
- if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
- return true, nil
- }
-
s.autoApproveSessionsMu.RLock()
autoApprove := s.autoApproveSessions[opts.SessionID]
s.autoApproveSessionsMu.RUnlock()
@@ -58,7 +58,7 @@ func (c *Capabilities) Update(msg any) {
}
case tea.TerminalVersionMsg:
c.TerminalVersion = m.Name
- case uv.ModeReportEvent:
+ case tea.ModeReportMsg:
switch m.Mode {
case ansi.ModeFocusEvent:
c.ReportFocusEvents = modeSupported(m.Value)
@@ -77,7 +77,7 @@ func QueryCmd(env uv.Environ) tea.Cmd {
shouldQueryFor := shouldQueryCapabilities(env)
if shouldQueryFor {
sb.WriteString(ansi.RequestNameVersion)
- // sb.WriteString(ansi.RequestModeFocusEvent) // TODO: re-enable when we need notifications.
+ sb.WriteString(ansi.RequestModeFocusEvent)
sb.WriteString(ansi.WindowOp(14)) // Window size in pixels
kittyReq := ansi.KittyGraphics([]byte("AAAA"), "i=31", "s=1", "v=1", "a=q", "t=d", "f=24")
if _, isTmux := env.LookupEnv("TMUX"); isTmux {
@@ -10,6 +10,8 @@ import (
"github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/ui/styles"
"github.com/charmbracelet/x/ansi"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
)
// PrettyPath formats a file path with home directory shortening and applies
@@ -19,6 +21,14 @@ func PrettyPath(t *styles.Styles, path string, width int) string {
return t.Muted.Width(width).Render(formatted)
}
+// FormatReasoningEffort formats a reasoning effort level for display.
+func FormatReasoningEffort(effort string) string {
+ if effort == "xhigh" {
+ return "X-High"
+ }
+ return cases.Title(language.English).String(effort)
+}
+
// ModelContextInfo contains token usage and cost information for a model.
type ModelContextInfo struct {
ContextUsed int64
@@ -13,8 +13,6 @@ import (
"github.com/charmbracelet/crush/internal/ui/styles"
uv "github.com/charmbracelet/ultraviolet"
"github.com/sahilm/fuzzy"
- "golang.org/x/text/cases"
- "golang.org/x/text/language"
)
const (
@@ -241,13 +239,12 @@ func (r *Reasoning) setReasoningItems() error {
currentEffort = model.DefaultReasoningEffort
}
- caser := cases.Title(language.English)
items := make([]list.FilterableItem, 0, len(model.ReasoningLevels))
selectedIndex := 0
for i, effort := range model.ReasoningLevels {
item := &ReasoningItem{
effort: effort,
- title: caser.String(effort),
+ title: common.FormatReasoningEffort(effort),
isCurrent: effort == currentEffort,
t: r.com.Styles,
}
@@ -9,8 +9,6 @@ import (
"github.com/charmbracelet/crush/internal/ui/logo"
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/ultraviolet/layout"
- "golang.org/x/text/cases"
- "golang.org/x/text/language"
)
// modelInfo renders the current model information including reasoning
@@ -35,9 +33,8 @@ func (m *UI) modelInfo(width int) string {
reasoningInfo = "Thinking Off"
}
} else {
- formatter := cases.Title(language.English, cases.NoLower)
reasoningEffort := cmp.Or(model.ModelCfg.ReasoningEffort, model.CatwalkCfg.DefaultReasoningEffort)
- reasoningInfo = formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))
+ reasoningInfo = fmt.Sprintf("Reasoning %s", common.FormatReasoningEffort(reasoningEffort))
}
}
}
@@ -100,9 +100,12 @@ func (s *Status) Draw(scr uv.Screen, area uv.Rectangle) {
}
ind := indStyle.String()
- messageWidth := max(0, area.Dx()-lipgloss.Width(ind)-msgStyle.GetHorizontalPadding())
- msg := ansi.Truncate(s.msg.Msg, messageWidth, "β¦")
- msg += strings.Repeat(" ", max(0, messageWidth-lipgloss.Width(msg)))
+ indWidth := lipgloss.Width(ind)
+ msg := strings.Join(strings.Split(s.msg.Msg, "\n"), " ")
+ msgWidth := lipgloss.Width(msg)
+ msg = ansi.Truncate(msg, area.Dx()-indWidth-msgWidth, "β¦")
+ padWidth := max(0, area.Dx()-indWidth-msgWidth)
+ msg += strings.Repeat(" ", padWidth)
info := msgStyle.Render(msg)
// Draw the info message over the help view
@@ -25,6 +25,7 @@ import (
tea "charm.land/bubbletea/v2"
"charm.land/catwalk/pkg/catwalk"
"charm.land/lipgloss/v2"
+ "github.com/charmbracelet/crush/internal/agent/notify"
agenttools "github.com/charmbracelet/crush/internal/agent/tools"
"github.com/charmbracelet/crush/internal/agent/tools/mcp"
"github.com/charmbracelet/crush/internal/app"
@@ -45,6 +46,7 @@ import (
"github.com/charmbracelet/crush/internal/ui/dialog"
fimage "github.com/charmbracelet/crush/internal/ui/image"
"github.com/charmbracelet/crush/internal/ui/logo"
+ "github.com/charmbracelet/crush/internal/ui/notification"
"github.com/charmbracelet/crush/internal/ui/styles"
"github.com/charmbracelet/crush/internal/ui/util"
"github.com/charmbracelet/crush/internal/version"
@@ -201,6 +203,9 @@ type UI struct {
// sidebarLogo keeps a cached version of the sidebar sidebarLogo.
sidebarLogo string
+ // Notification state
+ notifyBackend notification.Backend
+ notifyWindowFocused bool
// custom commands & mcp commands
customCommands []commands.CustomCommand
mcpPrompts []commands.MCPPrompt
@@ -280,17 +285,19 @@ func New(com *common.Common) *UI {
header := newHeader(com)
ui := &UI{
- com: com,
- dialog: dialog.NewOverlay(),
- keyMap: keyMap,
- textarea: ta,
- chat: ch,
- header: header,
- completions: comp,
- attachments: attachments,
- todoSpinner: todoSpinner,
- lspStates: make(map[string]app.LSPClientInfo),
- mcpStates: make(map[string]mcp.ClientInfo),
+ com: com,
+ dialog: dialog.NewOverlay(),
+ keyMap: keyMap,
+ textarea: ta,
+ chat: ch,
+ header: header,
+ completions: comp,
+ attachments: attachments,
+ todoSpinner: todoSpinner,
+ lspStates: make(map[string]app.LSPClientInfo),
+ mcpStates: make(map[string]mcp.ClientInfo),
+ notifyBackend: notification.NoopBackend{},
+ notifyWindowFocused: true,
}
status := NewStatus(com, ui)
@@ -342,6 +349,32 @@ func (m *UI) Init() tea.Cmd {
return tea.Batch(cmds...)
}
+// sendNotification returns a command that sends a notification if allowed by policy.
+func (m *UI) sendNotification(n notification.Notification) tea.Cmd {
+ if !m.shouldSendNotification() {
+ return nil
+ }
+
+ backend := m.notifyBackend
+ return func() tea.Msg {
+ if err := backend.Send(n); err != nil {
+ slog.Error("Failed to send notification", "error", err)
+ }
+ return nil
+ }
+}
+
+// shouldSendNotification returns true if notifications should be sent based on
+// current state. Focus reporting must be supported, window must not focused,
+// and notifications must not be disabled in config.
+func (m *UI) shouldSendNotification() bool {
+ cfg := m.com.Config()
+ if cfg != nil && cfg.Options != nil && cfg.Options.DisableNotifications {
+ return false
+ }
+ return m.caps.ReportFocusEvents && !m.notifyWindowFocused
+}
+
// setState changes the UI state and focus.
func (m *UI) setState(state uiState, focus uiFocusState) {
if state == uiLanding {
@@ -397,6 +430,18 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.sendProgressBar = slices.Contains(msg, "WT_SESSION")
}
cmds = append(cmds, common.QueryCmd(uv.Environ(msg)))
+ case tea.ModeReportMsg:
+ if m.caps.ReportFocusEvents {
+ m.notifyBackend = notification.NewNativeBackend(notification.Icon)
+ }
+ case tea.FocusMsg:
+ m.notifyWindowFocused = true
+ case tea.BlurMsg:
+ m.notifyWindowFocused = false
+ case pubsub.Event[notify.Notification]:
+ if cmd := m.handleAgentNotification(msg.Payload); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
case loadSessionMsg:
if m.forceCompactMode {
m.isCompact = true
@@ -540,6 +585,12 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if cmd := m.openPermissionsDialog(msg.Payload); cmd != nil {
cmds = append(cmds, cmd)
}
+ if cmd := m.sendNotification(notification.Notification{
+ Title: "Crush is waiting...",
+ Message: fmt.Sprintf("Permission required to execute \"%s\"", msg.Payload.ToolName),
+ }); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
case pubsub.Event[permission.PermissionNotification]:
m.handlePermissionNotification(msg.Payload)
case cancelTimerExpiredMsg:
@@ -1968,6 +2019,7 @@ func (m *UI) View() tea.View {
v.BackgroundColor = m.com.Styles.Background
}
v.MouseMode = tea.MouseModeCellMotion
+ v.ReportFocus = m.caps.ReportFocusEvents
v.WindowTitle = "crush " + home.Short(m.com.Config().WorkingDir())
canvas := uv.NewScreenBuffer(m.width, m.height)
@@ -2984,6 +3036,20 @@ func (m *UI) handlePermissionNotification(notification permission.PermissionNoti
}
}
+// handleAgentNotification translates domain agent events into desktop
+// notifications using the UI notification backend.
+func (m *UI) handleAgentNotification(n notify.Notification) tea.Cmd {
+ switch n.Type {
+ case notify.TypeAgentFinished:
+ return m.sendNotification(notification.Notification{
+ Title: "Crush is waiting...",
+ Message: fmt.Sprintf("Agent's turn completed in \"%s\"", n.SessionTitle),
+ })
+ default:
+ return nil
+ }
+}
+
// newSession clears the current session state and prepares for a new session.
// The actual session creation happens when the user sends their first message.
// Returns a command to reload prompt history.
@@ -3135,13 +3201,13 @@ func (m *UI) pasteImageFromClipboard() tea.Msg {
textData, textErr := readClipboard(clipboardFormatText)
if textErr != nil || len(textData) == 0 {
- return util.NewInfoMsg("Clipboard is empty or does not contain an image")
+ return nil // Clipboard is empty or does not contain an image
}
path := strings.TrimSpace(string(textData))
path = strings.ReplaceAll(path, "\\ ", " ")
if _, statErr := os.Stat(path); statErr != nil {
- return util.NewInfoMsg("Clipboard does not contain an image or valid file path")
+ return nil // Clipboard does not contain an image or valid file path
}
lowerPath := strings.ToLower(path)
@@ -0,0 +1,7 @@
+//go:build darwin
+
+package notification
+
+// Icon is currently empty on darwin because platform icon support is broken. Do
+// use the icon for OSC notifications, just not native.
+var Icon any = ""
@@ -0,0 +1,13 @@
+//go:build !darwin
+
+package notification
+
+import (
+ _ "embed"
+)
+
+//go:embed crush-icon-solo.png
+var icon []byte
+
+// Icon contains the embedded PNG icon data for desktop notifications.
+var Icon any = icon
@@ -0,0 +1,49 @@
+package notification
+
+import (
+ "log/slog"
+
+ "github.com/gen2brain/beeep"
+)
+
+// NativeBackend sends desktop notifications using the native OS notification
+// system via beeep.
+type NativeBackend struct {
+ // icon is the notification icon data (platform-specific).
+ icon any
+ // notifyFunc is the function used to send notifications (swappable for testing).
+ notifyFunc func(title, message string, icon any) error
+}
+
+// NewNativeBackend creates a new native notification backend.
+func NewNativeBackend(icon any) *NativeBackend {
+ beeep.AppName = "Crush"
+ return &NativeBackend{
+ icon: icon,
+ notifyFunc: beeep.Notify,
+ }
+}
+
+// Send sends a desktop notification using the native OS notification system.
+func (b *NativeBackend) Send(n Notification) error {
+ slog.Debug("Sending native notification", "title", n.Title, "message", n.Message)
+
+ err := b.notifyFunc(n.Title, n.Message, b.icon)
+ if err != nil {
+ slog.Error("Failed to send notification", "error", err)
+ } else {
+ slog.Debug("Notification sent successfully")
+ }
+
+ return err
+}
+
+// SetNotifyFunc allows replacing the notification function for testing.
+func (b *NativeBackend) SetNotifyFunc(fn func(title, message string, icon any) error) {
+ b.notifyFunc = fn
+}
+
+// ResetNotifyFunc resets the notification function to the default.
+func (b *NativeBackend) ResetNotifyFunc() {
+ b.notifyFunc = beeep.Notify
+}
@@ -0,0 +1,10 @@
+package notification
+
+// NoopBackend is a no-op notification backend that does nothing.
+// This is the default backend used when notifications are not supported.
+type NoopBackend struct{}
+
+// Send does nothing and returns nil.
+func (NoopBackend) Send(_ Notification) error {
+ return nil
+}
@@ -0,0 +1,15 @@
+// Package notification provides desktop notification support for the UI.
+package notification
+
+// Notification represents a desktop notification request.
+type Notification struct {
+ Title string
+ Message string
+}
+
+// Backend defines the interface for sending desktop notifications.
+// Implementations are pure transport - policy decisions (config, focus state)
+// are handled by the caller.
+type Backend interface {
+ Send(n Notification) error
+}
@@ -0,0 +1,43 @@
+package notification_test
+
+import (
+ "testing"
+
+ "github.com/charmbracelet/crush/internal/ui/notification"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNoopBackend_Send(t *testing.T) {
+ t.Parallel()
+
+ backend := notification.NoopBackend{}
+ err := backend.Send(notification.Notification{
+ Title: "Test Title",
+ Message: "Test Message",
+ })
+ require.NoError(t, err)
+}
+
+func TestNativeBackend_Send(t *testing.T) {
+ t.Parallel()
+
+ backend := notification.NewNativeBackend(nil)
+
+ var capturedTitle, capturedMessage string
+ var capturedIcon any
+ backend.SetNotifyFunc(func(title, message string, icon any) error {
+ capturedTitle = title
+ capturedMessage = message
+ capturedIcon = icon
+ return nil
+ })
+
+ err := backend.Send(notification.Notification{
+ Title: "Hello",
+ Message: "World",
+ })
+ require.NoError(t, err)
+ require.Equal(t, "Hello", capturedTitle)
+ require.Equal(t, "World", capturedMessage)
+ require.Nil(t, capturedIcon)
+}
@@ -451,6 +451,11 @@
"type": "boolean",
"description": "Show indeterminate progress updates during long operations",
"default": true
+ },
+ "disable_notifications": {
+ "type": "boolean",
+ "description": "Disable desktop notifications",
+ "default": false
}
},
"additionalProperties": false,