diff --git a/.github/cla-signatures.json b/.github/cla-signatures.json index c7037135e5d00cb257235b8574d145dec774b711..cd19b0bfe7c25bcd13fb6195e2d8903525303cfe 100644 --- a/.github/cla-signatures.json +++ b/.github/cla-signatures.json @@ -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 } ] } \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index ba2c56a67e35665b955683fcec659e1da0282ede..75b8023df6416ebfe776ce5daac758d1b7c38bef 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 0691c3b7bdef8bdd1adfde6cb75e5fd0e2e01d60..f6095e44bb08552c8dfa4e429762aa5422228951 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/README.md b/README.md index 49563822772d28762215f90f257e17a9710ac25f..46d5da5413ac3e30aaec094dbfb12d832d8647d5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/go.mod b/go.mod index 26fcbaf6e17fe9032e8c0f063eb69a360ca70734..9f91fda2fc86914d595bf8b5eaabaddcd7b05319 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index d759ae75ee70b1266f6a270d6e4784040afe348d..aff01decd2858103938d3c73f3584760189ccd91 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 46be4504923b42d753ac1f058648875622605d63..707298e612a65db62dd2d8c954c25e27e19a6009 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -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 diff --git a/internal/agent/common_test.go b/internal/agent/common_test.go index 101b987f2417659828fa68ae68405c1a723322b3..89fc6ff3d29d27c60a8091f17ebe0fad057dc44a 100644 --- a/internal/agent/common_test.go +++ b/internal/agent/common_test.go @@ -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 } diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index de34c6f037956e7b0a235c9d9f26aea4842467ff..2416d112127ce9d62b84f26c022fb5af9386f0df 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -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 diff --git a/internal/agent/notify/notify.go b/internal/agent/notify/notify.go new file mode 100644 index 0000000000000000000000000000000000000000..aba0069a1dc945dd42dd8f6a513095fa8d14157e --- /dev/null +++ b/internal/agent/notify/notify.go @@ -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 +} diff --git a/internal/app/app.go b/internal/app/app.go index 3755db0d5321029b88cc1c15d8fb99911f635bf3..7d87bd1231000cb2a1c88c1fa7a0ceae5b4316a9 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 16598f98765b321e6c7e5c9d8e51133800f57aa1..52ffda3fb09a0e6fdfb88084b80f7bdd261fb3c2 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -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) diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 50005a548bad0308bdca3a2afbe17503c1f86c56..1119a83f993d08b3a2206104912ec993ec37a9e3 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -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() { diff --git a/internal/config/config.go b/internal/config/config.go index c4ef08760ca329d5d0b5644985552e6013d9edd2..118afef344f8a022add7a13db406ccce27a1391e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 diff --git a/internal/event/event.go b/internal/event/event.go index aaa0d213fc49def2f1307c83366ef9bf19c4b1ae..516df804fbbca1bc03212b2c3cf26a38efab6979 100644 --- a/internal/event/event.go +++ b/internal/event/event.go @@ -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( diff --git a/internal/permission/permission.go b/internal/permission/permission.go index fc47b7dc93869a1b0a39d30ddb0e408ce479429f..a5d238b379137362dba4989a954f0b1fbb84979e 100644 --- a/internal/permission/permission.go +++ b/internal/permission/permission.go @@ -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() diff --git a/internal/ui/common/capabilities.go b/internal/ui/common/capabilities.go index b9a9de674d4b17e4ac59ff41a93b9f0db2e0028a..36d72f3e27f5d837107712849c3d4d7882c94cac 100644 --- a/internal/ui/common/capabilities.go +++ b/internal/ui/common/capabilities.go @@ -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 { diff --git a/internal/ui/common/elements.go b/internal/ui/common/elements.go index 1477b2a5208e31831a1725042daa6190364ced46..9c60a305d6d0c601efe2c57c363784d46eeda9f3 100644 --- a/internal/ui/common/elements.go +++ b/internal/ui/common/elements.go @@ -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 diff --git a/internal/ui/dialog/reasoning.go b/internal/ui/dialog/reasoning.go index cd8ba7d1e293fcb66d04c76f420463aa3a94770f..49a4711f9317fb69a84eefa4b1d5a216674a77ed 100644 --- a/internal/ui/dialog/reasoning.go +++ b/internal/ui/dialog/reasoning.go @@ -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, } diff --git a/internal/ui/model/sidebar.go b/internal/ui/model/sidebar.go index 221405a0a276a38c531223f7ed5adde1139c6ef8..88113a593034b09ed8d2859bc7628a103f5728b1 100644 --- a/internal/ui/model/sidebar.go +++ b/internal/ui/model/sidebar.go @@ -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)) } } } diff --git a/internal/ui/model/status.go b/internal/ui/model/status.go index fbec7792bd9f6fbc9445036323f9a425438c200d..ad6f0f81f776389b3aecfca560967924a47ace09 100644 --- a/internal/ui/model/status.go +++ b/internal/ui/model/status.go @@ -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 diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index c183344e36885baa85377da057f59590d95df088..c8433a2e93b047557535d1b1a9b8163ebe79690a 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -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) diff --git a/internal/ui/notification/crush-icon-solo.png b/internal/ui/notification/crush-icon-solo.png new file mode 100644 index 0000000000000000000000000000000000000000..eed026660d0d5882c9b6e98912ee2afd9748f2a6 Binary files /dev/null and b/internal/ui/notification/crush-icon-solo.png differ diff --git a/internal/ui/notification/crush-icon.png b/internal/ui/notification/crush-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..138a7ab6d246a0989cf9fca296e7c7df78523465 Binary files /dev/null and b/internal/ui/notification/crush-icon.png differ diff --git a/internal/ui/notification/icon_darwin.go b/internal/ui/notification/icon_darwin.go new file mode 100644 index 0000000000000000000000000000000000000000..27df25009be6bb849afc7b39b631fbbe3c61b6b3 --- /dev/null +++ b/internal/ui/notification/icon_darwin.go @@ -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 = "" diff --git a/internal/ui/notification/icon_other.go b/internal/ui/notification/icon_other.go new file mode 100644 index 0000000000000000000000000000000000000000..27240ad93fc653c9e742a879e76914481e5f1d55 --- /dev/null +++ b/internal/ui/notification/icon_other.go @@ -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 diff --git a/internal/ui/notification/native.go b/internal/ui/notification/native.go new file mode 100644 index 0000000000000000000000000000000000000000..4fffa6d2de6798f8c343c3789689844a911b6eb0 --- /dev/null +++ b/internal/ui/notification/native.go @@ -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 +} diff --git a/internal/ui/notification/noop.go b/internal/ui/notification/noop.go new file mode 100644 index 0000000000000000000000000000000000000000..7e943e38af15ad4e2dcd47c95158bb4abcb6bb56 --- /dev/null +++ b/internal/ui/notification/noop.go @@ -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 +} diff --git a/internal/ui/notification/notification.go b/internal/ui/notification/notification.go new file mode 100644 index 0000000000000000000000000000000000000000..f6be12bfe8b84c2cf18b4c5f1ae3720e820e6cd5 --- /dev/null +++ b/internal/ui/notification/notification.go @@ -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 +} diff --git a/internal/ui/notification/notification_test.go b/internal/ui/notification/notification_test.go new file mode 100644 index 0000000000000000000000000000000000000000..715be608c75328e3bc2b9e820c301a62a17f08a5 --- /dev/null +++ b/internal/ui/notification/notification_test.go @@ -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) +} diff --git a/schema.json b/schema.json index 298d8fe814b80fa693759e9de5a1dafb921b389f..3f9754158f3bc91cc1b6570d5e5393a6b594c22d 100644 --- a/schema.json +++ b/schema.json @@ -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,