From 893e65bc97088a40d2d51d907331212b5015d751 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 11 Feb 2026 14:54:26 +0100 Subject: [PATCH] Bump Go to 1.26.0 Signed-off-by: David Gageot --- .github/workflows/ci.yml | 10 +-- AGENTS.md | 2 +- Dockerfile | 2 +- cmd/root/root.go | 12 ++-- cmd/root/run.go | 3 +- go.mod | 2 +- pkg/acp/agent.go | 8 +-- pkg/chat/chat.go | 7 +- pkg/config/config.go | 6 +- pkg/config/latest/schema_test.go | 59 ++++++++--------- pkg/config/latest/types.go | 33 +++++----- pkg/config/sources.go | 8 +-- pkg/config/v0/types.go | 6 +- pkg/config/v1/types.go | 4 +- pkg/config/v2/types.go | 25 ++++--- pkg/config/v3/types.go | 31 +++++---- pkg/config/v4/schema_test.go | 59 ++++++++--------- pkg/config/v4/types.go | 33 +++++----- pkg/evaluation/build.go | 3 +- pkg/evaluation/judge.go | 6 +- pkg/gateway/types.go | 2 +- pkg/hooks/executor.go | 18 ++--- pkg/hooks/hooks_test.go | 8 +-- pkg/model/provider/anthropic/adapter.go | 4 +- pkg/model/provider/anthropic/files.go | 8 +-- pkg/model/provider/dmr/client.go | 2 +- pkg/model/provider/dmr/client_test.go | 26 +++----- pkg/model/provider/gemini/client.go | 14 ++-- pkg/model/provider/gemini/client_test.go | 15 ++--- pkg/model/provider/model_defaults_test.go | 22 ++----- pkg/model/provider/override_test.go | 24 +++---- pkg/model/provider/provider.go | 12 ++-- pkg/modelsdev/store.go | 6 +- pkg/rag/treesitter/treesitter.go | 4 +- pkg/runtime/fallback.go | 9 +-- pkg/runtime/runtime_test.go | 2 +- pkg/session/branch.go | 5 +- pkg/sessiontitle/generator.go | 4 +- pkg/sqliteutil/sqlite.go | 6 +- pkg/teamloader/teamloader.go | 3 +- pkg/telemetry/http.go | 5 +- pkg/tools/builtin/lsp.go | 7 +- pkg/tools/builtin/shell.go | 3 +- pkg/tools/mcp/tokenstore.go | 2 +- pkg/tui/components/markdown/fast_renderer.go | 31 ++++----- .../components/markdown/fast_renderer_test.go | 17 ++--- pkg/tui/components/message/message.go | 5 +- pkg/tui/components/messages/messages.go | 18 +++-- pkg/tui/components/messages/messages_test.go | 17 ++--- pkg/tui/components/sidebar/sidebar.go | 19 ++---- pkg/tui/dialog/model_picker.go | 4 +- pkg/tui/dialog/multi_choice.go | 27 ++------ pkg/tui/page/chat/chat.go | 8 +-- pkg/tui/styles/styles.go | 38 +++++------ pkg/tui/styles/theme.go | 24 +++---- pkg/tui/styles/theme_test.go | 66 +++++++------------ 56 files changed, 331 insertions(+), 473 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0811487c3..84e9d235c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,13 +25,13 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: "1.25.5" + go-version: "1.26.0" cache: true - name: Lint uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0 with: - version: v2.8 + version: v2.9 - name: Lint GitHub Actions uses: raven-actions/actionlint@e01d1ea33dd6a5ed517d95b4c0c357560ac6f518 # v2.1.1 @@ -48,7 +48,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: "1.25.5" + go-version: "1.26.0" cache: true - name: Install Task @@ -68,7 +68,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: "1.25.5" + go-version: "1.26.0" cache: true - name: Install go-licences @@ -87,7 +87,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: "1.25.5" + go-version: "1.26.0" cache: true - name: Install Task diff --git a/AGENTS.md b/AGENTS.md index c4fc46883..f651ca486 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -849,7 +849,7 @@ task build # Should create ./bin/cagent - Manual workflow dispatch **Build Configuration:** -- Go version: 1.25.5 +- Go version: 1.26.0 - Platforms: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64 - Binary name: `cagent` (or `cagent.exe` on Windows) - Version injection: Uses git tag and commit SHA via ldflags diff --git a/Dockerfile b/Dockerfile index 9e5bec82f..dd562ef07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION="1.25.5" +ARG GO_VERSION="1.26.0" ARG ALPINE_VERSION="3.22" ARG XX_VERSION="1.9.0" diff --git a/cmd/root/root.go b/cmd/root/root.go index ab04c5310..9f2993396 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -142,22 +142,18 @@ We collect anonymous usage data to help improve cagent. To disable: rootCmd.SetErr(stderr) if err := rootCmd.ExecuteContext(ctx); err != nil { - envErr := &environment.RequiredEnvError{} - runtimeErr := RuntimeError{} - - switch { - case ctx.Err() != nil: + if ctx.Err() != nil { return ctx.Err() - case errors.As(err, &envErr): + } else if envErr, ok := errors.AsType[*environment.RequiredEnvError](err); ok { fmt.Fprintln(stderr, "The following environment variables must be set:") for _, v := range envErr.Missing { fmt.Fprintf(stderr, " - %s\n", v) } fmt.Fprintln(stderr, "\nEither:\n - Set those environment variables before running cagent\n - Run cagent with --env-from-file\n - Store those secrets using one of the built-in environment variable providers.") - case errors.As(err, &runtimeErr): + } else if _, ok := errors.AsType[RuntimeError](err); ok { // Runtime errors have already been printed by the command itself // Don't print them again or show usage - default: + } else { // Command line usage errors - show the error and usage fmt.Fprintln(stderr, err) fmt.Fprintln(stderr) diff --git a/cmd/root/run.go b/cmd/root/run.go index fafaab6cd..48e045e04 100644 --- a/cmd/root/run.go +++ b/cmd/root/run.go @@ -443,8 +443,7 @@ func (f *runExecFlags) handleExecMode(ctx context.Context, out *cli.Printer, rt OutputJSON: f.outputJSON, AutoApprove: f.autoApprove, }, rt, sess, userMessages) - var cliErr cli.RuntimeError - if errors.As(err, &cliErr) { + if cliErr, ok := errors.AsType[cli.RuntimeError](err); ok { return RuntimeError{Err: cliErr.Err} } return err diff --git a/go.mod b/go.mod index 16e5ecf29..bcb4244c9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/docker/cagent -go 1.25.5 +go 1.26.0 replace github.com/charmbracelet/ultraviolet => github.com/dgageot/ultraviolet v0.0.0-20260106070720-e493364e381d diff --git a/pkg/acp/agent.go b/pkg/acp/agent.go index f747b21d3..86666a306 100644 --- a/pkg/acp/agent.go +++ b/pkg/acp/agent.go @@ -513,7 +513,7 @@ func (a *Agent) handleMaxIterationsReached(ctx context.Context, acpSess *Session SessionId: acp.SessionId(acpSess.id), ToolCall: acp.RequestPermissionToolCall{ ToolCallId: "max_iterations", - Title: acp.Ptr(fmt.Sprintf("Maximum iterations (%d) reached", e.MaxIterations)), + Title: new(fmt.Sprintf("Maximum iterations (%d) reached", e.MaxIterations)), Kind: acp.Ptr(acp.ToolKindExecute), Status: acp.Ptr(acp.ToolCallStatusPending), }, @@ -750,9 +750,9 @@ func buildToolCallUpdate(toolCall tools.ToolCall, tool tools.Tool, status acp.To return acp.RequestPermissionToolCall{ ToolCallId: acp.ToolCallId(toolCall.ID), - Title: acp.Ptr(title), - Kind: acp.Ptr(kind), - Status: acp.Ptr(status), + Title: &title, + Kind: &kind, + Status: &status, RawInput: parseToolCallArguments(toolCall.Function.Arguments), } } diff --git a/pkg/chat/chat.go b/pkg/chat/chat.go index 9762243d0..91a90ca9d 100644 --- a/pkg/chat/chat.go +++ b/pkg/chat/chat.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "unicode/utf8" @@ -336,10 +337,8 @@ func isTextByContent(filePath string) bool { data := buf[:n] // Check for null bytes (strong binary indicator) - for _, b := range data { - if b == 0 { - return false - } + if slices.Contains(data, 0) { + return false } // Check if the content is valid UTF-8 diff --git a/pkg/config/config.go b/pkg/config/config.go index 6d573ce4a..147534d75 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -105,7 +105,7 @@ func validateConfig(cfg *latest.Config) error { for name := range cfg.Models { if cfg.Models[name].ParallelToolCalls == nil { m := cfg.Models[name] - m.ParallelToolCalls = boolPtr(true) + m.ParallelToolCalls = new(true) cfg.Models[name] = m } } @@ -134,10 +134,6 @@ func validateConfig(cfg *latest.Config) error { return nil } -func boolPtr(b bool) *bool { - return &b -} - // providerAPITypes are the allowed values for api_type in provider configs var providerAPITypes = map[string]bool{ "": true, // empty is allowed (defaults to openai_chatcompletions) diff --git a/pkg/config/latest/schema_test.go b/pkg/config/latest/schema_test.go index ec131e424..1874902b0 100644 --- a/pkg/config/latest/schema_test.go +++ b/pkg/config/latest/schema_test.go @@ -2,6 +2,7 @@ package latest import ( "encoding/json" + "maps" "os" "reflect" "sort" @@ -46,18 +47,14 @@ func (s jsonSchema) resolveRef(root jsonSchema) jsonSchema { // excluded. It recurses into anonymous (embedded) struct fields so that // promoted fields are included. func structJSONFields(t reflect.Type) map[string]bool { - if t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Pointer { t = t.Elem() } fields := make(map[string]bool) - for i := range t.NumField() { - f := t.Field(i) - + for f := range t.Fields() { // Recurse into anonymous (embedded) structs. if f.Anonymous { - for k, v := range structJSONFields(f.Type) { - fields[k] = v - } + maps.Copy(fields, structJSONFields(f.Type)) continue } @@ -119,27 +116,27 @@ func TestSchemaMatchesGoTypes(t *testing.T) { entries := []entry{ // Top-level Config - {reflect.TypeOf(Config{}), root, "Config (top-level)"}, + {reflect.TypeFor[Config](), root, "Config (top-level)"}, } // Definitions that map 1:1 to a Go struct. definitionMap := map[string]reflect.Type{ - "AgentConfig": reflect.TypeOf(AgentConfig{}), - "FallbackConfig": reflect.TypeOf(FallbackConfig{}), - "ModelConfig": reflect.TypeOf(ModelConfig{}), - "Metadata": reflect.TypeOf(Metadata{}), - "ProviderConfig": reflect.TypeOf(ProviderConfig{}), - "Toolset": reflect.TypeOf(Toolset{}), - "Remote": reflect.TypeOf(Remote{}), - "SandboxConfig": reflect.TypeOf(SandboxConfig{}), - "ScriptShellToolConfig": reflect.TypeOf(ScriptShellToolConfig{}), - "PostEditConfig": reflect.TypeOf(PostEditConfig{}), - "PermissionsConfig": reflect.TypeOf(PermissionsConfig{}), - "HooksConfig": reflect.TypeOf(HooksConfig{}), - "HookMatcherConfig": reflect.TypeOf(HookMatcherConfig{}), - "HookDefinition": reflect.TypeOf(HookDefinition{}), - "RoutingRule": reflect.TypeOf(RoutingRule{}), - "ApiConfig": reflect.TypeOf(APIToolConfig{}), + "AgentConfig": reflect.TypeFor[AgentConfig](), + "FallbackConfig": reflect.TypeFor[FallbackConfig](), + "ModelConfig": reflect.TypeFor[ModelConfig](), + "Metadata": reflect.TypeFor[Metadata](), + "ProviderConfig": reflect.TypeFor[ProviderConfig](), + "Toolset": reflect.TypeFor[Toolset](), + "Remote": reflect.TypeFor[Remote](), + "SandboxConfig": reflect.TypeFor[SandboxConfig](), + "ScriptShellToolConfig": reflect.TypeFor[ScriptShellToolConfig](), + "PostEditConfig": reflect.TypeFor[PostEditConfig](), + "PermissionsConfig": reflect.TypeFor[PermissionsConfig](), + "HooksConfig": reflect.TypeFor[HooksConfig](), + "HookMatcherConfig": reflect.TypeFor[HookMatcherConfig](), + "HookDefinition": reflect.TypeFor[HookDefinition](), + "RoutingRule": reflect.TypeFor[RoutingRule](), + "ApiConfig": reflect.TypeFor[APIToolConfig](), } for name, goType := range definitionMap { @@ -159,13 +156,13 @@ func TestSchemaMatchesGoTypes(t *testing.T) { } inlines := []inlineEntry{ - {reflect.TypeOf(StructuredOutput{}), []string{"AgentConfig", "structured_output"}, "StructuredOutput (AgentConfig.structured_output)"}, - {reflect.TypeOf(RAGConfig{}), []string{"RAGConfig"}, "RAGConfig"}, - {reflect.TypeOf(RAGToolConfig{}), []string{"RAGConfig", "tool"}, "RAGToolConfig (RAGConfig.tool)"}, - {reflect.TypeOf(RAGResultsConfig{}), []string{"RAGConfig", "results"}, "RAGResultsConfig (RAGConfig.results)"}, - {reflect.TypeOf(RAGFusionConfig{}), []string{"RAGConfig", "results", "fusion"}, "RAGFusionConfig (RAGConfig.results.fusion)"}, - {reflect.TypeOf(RAGRerankingConfig{}), []string{"RAGConfig", "results", "reranking"}, "RAGRerankingConfig (RAGConfig.results.reranking)"}, - {reflect.TypeOf(RAGChunkingConfig{}), []string{"RAGConfig", "strategies", "*", "chunking"}, "RAGChunkingConfig (RAGConfig.strategies[].chunking)"}, + {reflect.TypeFor[StructuredOutput](), []string{"AgentConfig", "structured_output"}, "StructuredOutput (AgentConfig.structured_output)"}, + {reflect.TypeFor[RAGConfig](), []string{"RAGConfig"}, "RAGConfig"}, + {reflect.TypeFor[RAGToolConfig](), []string{"RAGConfig", "tool"}, "RAGToolConfig (RAGConfig.tool)"}, + {reflect.TypeFor[RAGResultsConfig](), []string{"RAGConfig", "results"}, "RAGResultsConfig (RAGConfig.results)"}, + {reflect.TypeFor[RAGFusionConfig](), []string{"RAGConfig", "results", "fusion"}, "RAGFusionConfig (RAGConfig.results.fusion)"}, + {reflect.TypeFor[RAGRerankingConfig](), []string{"RAGConfig", "results", "reranking"}, "RAGRerankingConfig (RAGConfig.results.reranking)"}, + {reflect.TypeFor[RAGChunkingConfig](), []string{"RAGConfig", "strategies", "*", "chunking"}, "RAGChunkingConfig (RAGConfig.strategies[].chunking)"}, } for _, il := range inlines { diff --git a/pkg/config/latest/types.go b/pkg/config/latest/types.go index 937d34859..1823c0681 100644 --- a/pkg/config/latest/types.go +++ b/pkg/config/latest/types.go @@ -3,6 +3,7 @@ package latest import ( "encoding/json" "fmt" + "maps" "strings" "time" @@ -20,7 +21,7 @@ type Config struct { Providers map[string]ProviderConfig `json:"providers,omitempty"` Models map[string]ModelConfig `json:"models,omitempty"` RAG map[string]RAGConfig `json:"rag,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` + Metadata Metadata `json:"metadata"` Permissions *PermissionsConfig `json:"permissions,omitempty"` } @@ -125,7 +126,7 @@ type FallbackConfig struct { // Cooldown is the duration to stick with a successful fallback model before // retrying the primary. Only applies after a non-retryable error (e.g., 429). // Default is 1 minute. Use Go duration format (e.g., "1m", "30s", "2m30s"). - Cooldown Duration `json:"cooldown,omitempty"` + Cooldown Duration `json:"cooldown"` } // Duration is a wrapper around time.Duration that supports YAML/JSON unmarshaling @@ -410,13 +411,13 @@ type Toolset struct { Instruction string `json:"instruction,omitempty"` Toon string `json:"toon,omitempty"` - Defer DeferConfig `json:"defer,omitempty" yaml:"defer,omitempty"` + Defer DeferConfig `json:"defer" yaml:"defer,omitempty"` // For the `mcp` tool Command string `json:"command,omitempty"` Args []string `json:"args,omitempty"` Ref string `json:"ref,omitempty"` - Remote Remote `json:"remote,omitempty"` + Remote Remote `json:"remote"` Config any `json:"config,omitempty"` // For the `a2a` tool @@ -441,7 +442,7 @@ type Toolset struct { // For the `filesystem` tool - post-edit commands PostEdit []PostEditConfig `json:"post_edit,omitempty"` - APIConfig APIToolConfig `json:"api_config,omitempty"` + APIConfig APIToolConfig `json:"api_config"` // For the `filesystem` tool - VCS integration IgnoreVCS *bool `json:"ignore_vcs,omitempty"` @@ -569,11 +570,11 @@ func (t ThinkingBudget) MarshalYAML() ([]byte, error) { func (t ThinkingBudget) MarshalJSON() ([]byte, error) { // If Effort string is set (non-empty), marshal as string if t.Effort != "" { - return []byte(fmt.Sprintf("%q", t.Effort)), nil + return fmt.Appendf(nil, "%q", t.Effort), nil } // Otherwise marshal as integer (includes 0, -1, and positive values) - return []byte(fmt.Sprintf("%d", t.Tokens)), nil + return fmt.Appendf(nil, "%d", t.Tokens), nil } // UnmarshalJSON implements custom unmarshaling to accept simple string or int format @@ -618,11 +619,11 @@ type RAGToolConfig struct { // RAGConfig represents a RAG (Retrieval-Augmented Generation) configuration // Uses a unified strategies array for flexible, extensible configuration type RAGConfig struct { - Tool RAGToolConfig `json:"tool,omitempty"` // Tool configuration + Tool RAGToolConfig `json:"tool"` // Tool configuration Docs []string `json:"docs,omitempty"` // Shared documents across all strategies RespectVCS *bool `json:"respect_vcs,omitempty"` // Whether to respect VCS ignore files like .gitignore (default: true) Strategies []RAGStrategyConfig `json:"strategies,omitempty"` // Array of strategy configurations - Results RAGResultsConfig `json:"results,omitempty"` + Results RAGResultsConfig `json:"results"` } // GetRespectVCS returns whether VCS ignore files should be respected, defaulting to true @@ -636,11 +637,11 @@ func (c *RAGConfig) GetRespectVCS() bool { // RAGStrategyConfig represents a single retrieval strategy configuration // Strategy-specific fields are stored in Params (validated by strategy implementation) type RAGStrategyConfig struct { //nolint:recvcheck // Marshal methods must use value receiver for YAML/JSON slice encoding, Unmarshal must use pointer - Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc. - Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs) - Database RAGDatabaseConfig `json:"database,omitempty"` // Database configuration - Chunking RAGChunkingConfig `json:"chunking,omitempty"` // Chunking configuration - Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input) + Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc. + Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs) + Database RAGDatabaseConfig `json:"database"` // Database configuration + Chunking RAGChunkingConfig `json:"chunking"` // Chunking configuration + Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input) // Strategy-specific parameters (arbitrary key-value pairs) // Examples: @@ -793,9 +794,7 @@ func (s RAGStrategyConfig) buildFlattenedMap() map[string]any { } // Flatten Params into the same level - for k, v := range s.Params { - result[k] = v - } + maps.Copy(result, s.Params) return result } diff --git a/pkg/config/sources.go b/pkg/config/sources.go index c8d98e226..9325121bf 100644 --- a/pkg/config/sources.go +++ b/pkg/config/sources.go @@ -12,6 +12,7 @@ import ( "net/url" "os" "path/filepath" + "slices" "strings" "github.com/docker/cagent/pkg/content" @@ -295,12 +296,7 @@ func isGitHubURL(urlStr string) bool { if err != nil { return false } - for _, host := range githubHosts { - if u.Host == host { - return true - } - } - return false + return slices.Contains(githubHosts, u.Host) } // addGitHubAuth adds GitHub token authorization to the request if: diff --git a/pkg/config/v0/types.go b/pkg/config/v0/types.go index d0cbf519c..8d6738009 100644 --- a/pkg/config/v0/types.go +++ b/pkg/config/v0/types.go @@ -12,7 +12,7 @@ const Version = "0" type Toolset struct { Type string `json:"type,omitempty" yaml:"type,omitempty"` Command string `json:"command,omitempty" yaml:"command,omitempty"` - Remote Remote `json:"remote,omitempty" yaml:"remote,omitempty"` + Remote Remote `json:"remote" yaml:"remote,omitempty"` Args []string `json:"args,omitempty" yaml:"args,omitempty"` Env map[string]string `json:"env,omitempty" yaml:"env,omitempty"` Envfiles types.StringOrList `json:"env_file,omitempty" yaml:"env_file,omitempty"` @@ -87,8 +87,8 @@ type AgentConfig struct { SubAgents []string `json:"sub_agents,omitempty" yaml:"sub_agents,omitempty"` AddDate bool `json:"add_date,omitempty" yaml:"add_date,omitempty"` Think bool `json:"think,omitempty" yaml:"think,omitempty"` - Todo TodoConfig `json:"todo,omitempty" yaml:"todo,omitempty"` - MemoryConfig MemoryConfig `json:"memory,omitempty" yaml:"memory,omitempty"` + Todo TodoConfig `json:"todo" yaml:"todo,omitempty"` + MemoryConfig MemoryConfig `json:"memory" yaml:"memory,omitempty"` NumHistoryItems int `json:"num_history_items,omitempty" yaml:"num_history_items,omitempty"` Commands types.Commands `json:"commands,omitempty" yaml:"commands,omitempty"` } diff --git a/pkg/config/v1/types.go b/pkg/config/v1/types.go index dd5d52069..33086213d 100644 --- a/pkg/config/v1/types.go +++ b/pkg/config/v1/types.go @@ -36,7 +36,7 @@ type Toolset struct { Ref string `json:"ref,omitempty" yaml:"ref,omitempty"` Config any `json:"config,omitempty" yaml:"config,omitempty"` Command string `json:"command,omitempty" yaml:"command,omitempty"` - Remote Remote `json:"remote,omitempty" yaml:"remote,omitempty"` + Remote Remote `json:"remote" yaml:"remote,omitempty"` Args []string `json:"args,omitempty" yaml:"args,omitempty"` Env map[string]string `json:"env,omitempty" yaml:"env,omitempty"` Envfiles types.StringOrList `json:"env_file,omitempty" yaml:"env_file,omitempty"` @@ -139,7 +139,7 @@ type Config struct { Agents map[string]AgentConfig `json:"agents,omitempty" yaml:"agents,omitempty"` Models map[string]ModelConfig `json:"models,omitempty" yaml:"models,omitempty"` Env map[string]string `json:"env,omitempty" yaml:"env,omitempty"` - Metadata Metadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Metadata Metadata `json:"metadata" yaml:"metadata,omitempty"` } type Metadata struct { diff --git a/pkg/config/v2/types.go b/pkg/config/v2/types.go index fb100d361..b320ecc79 100644 --- a/pkg/config/v2/types.go +++ b/pkg/config/v2/types.go @@ -3,6 +3,7 @@ package v2 import ( "encoding/json" "fmt" + "maps" "github.com/goccy/go-yaml" @@ -17,7 +18,7 @@ type Config struct { Agents map[string]AgentConfig `json:"agents,omitempty"` Models map[string]ModelConfig `json:"models,omitempty"` RAG map[string]RAGConfig `json:"rag,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` + Metadata Metadata `json:"metadata"` } // AgentConfig represents a single agent configuration @@ -125,7 +126,7 @@ type Toolset struct { Command string `json:"command,omitempty"` Args []string `json:"args,omitempty"` Ref string `json:"ref,omitempty"` - Remote Remote `json:"remote,omitempty"` + Remote Remote `json:"remote"` Config any `json:"config,omitempty"` // For `shell`, `script` or `mcp` tools @@ -202,11 +203,11 @@ func (t *ThinkingBudget) UnmarshalYAML(unmarshal func(any) error) error { func (t ThinkingBudget) MarshalJSON() ([]byte, error) { // If Effort string is set (non-empty), marshal as string if t.Effort != "" { - return []byte(fmt.Sprintf("%q", t.Effort)), nil + return fmt.Appendf(nil, "%q", t.Effort), nil } // Otherwise marshal as integer (includes 0, -1, and positive values) - return []byte(fmt.Sprintf("%d", t.Tokens)), nil + return fmt.Appendf(nil, "%d", t.Tokens), nil } // UnmarshalJSON implements custom unmarshaling to accept simple string or int format @@ -246,17 +247,17 @@ type RAGConfig struct { Description string `json:"description,omitempty"` Docs []string `json:"docs,omitempty"` // Shared documents across all strategies Strategies []RAGStrategyConfig `json:"strategies,omitempty"` // Array of strategy configurations - Results RAGResultsConfig `json:"results,omitempty"` + Results RAGResultsConfig `json:"results"` } // RAGStrategyConfig represents a single retrieval strategy configuration // Strategy-specific fields are stored in Params (validated by strategy implementation) type RAGStrategyConfig struct { //nolint:recvcheck // Marshal methods must use value receiver for YAML/JSON slice encoding, Unmarshal must use pointer - Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc. - Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs) - Database RAGDatabaseConfig `json:"database,omitempty"` // Database configuration - Chunking RAGChunkingConfig `json:"chunking,omitempty"` // Chunking configuration - Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input) + Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc. + Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs) + Database RAGDatabaseConfig `json:"database"` // Database configuration + Chunking RAGChunkingConfig `json:"chunking"` // Chunking configuration + Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input) // Strategy-specific parameters (arbitrary key-value pairs) // Examples: @@ -409,9 +410,7 @@ func (s RAGStrategyConfig) buildFlattenedMap() map[string]any { } // Flatten Params into the same level - for k, v := range s.Params { - result[k] = v - } + maps.Copy(result, s.Params) return result } diff --git a/pkg/config/v3/types.go b/pkg/config/v3/types.go index 54d084c98..a2951ded8 100644 --- a/pkg/config/v3/types.go +++ b/pkg/config/v3/types.go @@ -3,6 +3,7 @@ package v3 import ( "encoding/json" "fmt" + "maps" "github.com/goccy/go-yaml" @@ -18,7 +19,7 @@ type Config struct { Agents map[string]AgentConfig `json:"agents,omitempty"` Models map[string]ModelConfig `json:"models,omitempty"` RAG map[string]RAGConfig `json:"rag,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` + Metadata Metadata `json:"metadata"` Permissions *PermissionsConfig `json:"permissions,omitempty"` } @@ -156,13 +157,13 @@ type Toolset struct { Instruction string `json:"instruction,omitempty"` Toon string `json:"toon,omitempty"` - Defer DeferConfig `json:"defer,omitempty" yaml:"defer,omitempty"` + Defer DeferConfig `json:"defer" yaml:"defer,omitempty"` // For the `mcp` tool Command string `json:"command,omitempty"` Args []string `json:"args,omitempty"` Ref string `json:"ref,omitempty"` - Remote Remote `json:"remote,omitempty"` + Remote Remote `json:"remote"` Config any `json:"config,omitempty"` // For the `a2a` tool @@ -187,7 +188,7 @@ type Toolset struct { // For the `filesystem` tool - post-edit commands PostEdit []PostEditConfig `json:"post_edit,omitempty"` - APIConfig APIToolConfig `json:"api_config,omitempty"` + APIConfig APIToolConfig `json:"api_config"` // For the `filesystem` tool - VCS integration IgnoreVCS *bool `json:"ignore_vcs,omitempty"` @@ -315,11 +316,11 @@ func (t ThinkingBudget) MarshalYAML() ([]byte, error) { func (t ThinkingBudget) MarshalJSON() ([]byte, error) { // If Effort string is set (non-empty), marshal as string if t.Effort != "" { - return []byte(fmt.Sprintf("%q", t.Effort)), nil + return fmt.Appendf(nil, "%q", t.Effort), nil } // Otherwise marshal as integer (includes 0, -1, and positive values) - return []byte(fmt.Sprintf("%d", t.Tokens)), nil + return fmt.Appendf(nil, "%d", t.Tokens), nil } // UnmarshalJSON implements custom unmarshaling to accept simple string or int format @@ -364,11 +365,11 @@ type RAGToolConfig struct { // RAGConfig represents a RAG (Retrieval-Augmented Generation) configuration // Uses a unified strategies array for flexible, extensible configuration type RAGConfig struct { - Tool RAGToolConfig `json:"tool,omitempty"` // Tool configuration + Tool RAGToolConfig `json:"tool"` // Tool configuration Docs []string `json:"docs,omitempty"` // Shared documents across all strategies RespectVCS *bool `json:"respect_vcs,omitempty"` // Whether to respect VCS ignore files like .gitignore (default: true) Strategies []RAGStrategyConfig `json:"strategies,omitempty"` // Array of strategy configurations - Results RAGResultsConfig `json:"results,omitempty"` + Results RAGResultsConfig `json:"results"` } // GetRespectVCS returns whether VCS ignore files should be respected, defaulting to true @@ -382,11 +383,11 @@ func (c *RAGConfig) GetRespectVCS() bool { // RAGStrategyConfig represents a single retrieval strategy configuration // Strategy-specific fields are stored in Params (validated by strategy implementation) type RAGStrategyConfig struct { //nolint:recvcheck // Marshal methods must use value receiver for YAML/JSON slice encoding, Unmarshal must use pointer - Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc. - Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs) - Database RAGDatabaseConfig `json:"database,omitempty"` // Database configuration - Chunking RAGChunkingConfig `json:"chunking,omitempty"` // Chunking configuration - Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input) + Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc. + Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs) + Database RAGDatabaseConfig `json:"database"` // Database configuration + Chunking RAGChunkingConfig `json:"chunking"` // Chunking configuration + Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input) // Strategy-specific parameters (arbitrary key-value pairs) // Examples: @@ -539,9 +540,7 @@ func (s RAGStrategyConfig) buildFlattenedMap() map[string]any { } // Flatten Params into the same level - for k, v := range s.Params { - result[k] = v - } + maps.Copy(result, s.Params) return result } diff --git a/pkg/config/v4/schema_test.go b/pkg/config/v4/schema_test.go index 9c84e9406..2ba0f8f87 100644 --- a/pkg/config/v4/schema_test.go +++ b/pkg/config/v4/schema_test.go @@ -2,6 +2,7 @@ package v4 import ( "encoding/json" + "maps" "os" "reflect" "sort" @@ -46,18 +47,14 @@ func (s jsonSchema) resolveRef(root jsonSchema) jsonSchema { // excluded. It recurses into anonymous (embedded) struct fields so that // promoted fields are included. func structJSONFields(t reflect.Type) map[string]bool { - if t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Pointer { t = t.Elem() } fields := make(map[string]bool) - for i := range t.NumField() { - f := t.Field(i) - + for f := range t.Fields() { // Recurse into anonymous (embedded) structs. if f.Anonymous { - for k, v := range structJSONFields(f.Type) { - fields[k] = v - } + maps.Copy(fields, structJSONFields(f.Type)) continue } @@ -119,27 +116,27 @@ func TestSchemaMatchesGoTypes(t *testing.T) { entries := []entry{ // Top-level Config - {reflect.TypeOf(Config{}), root, "Config (top-level)"}, + {reflect.TypeFor[Config](), root, "Config (top-level)"}, } // Definitions that map 1:1 to a Go struct. definitionMap := map[string]reflect.Type{ - "AgentConfig": reflect.TypeOf(AgentConfig{}), - "FallbackConfig": reflect.TypeOf(FallbackConfig{}), - "ModelConfig": reflect.TypeOf(ModelConfig{}), - "Metadata": reflect.TypeOf(Metadata{}), - "ProviderConfig": reflect.TypeOf(ProviderConfig{}), - "Toolset": reflect.TypeOf(Toolset{}), - "Remote": reflect.TypeOf(Remote{}), - "SandboxConfig": reflect.TypeOf(SandboxConfig{}), - "ScriptShellToolConfig": reflect.TypeOf(ScriptShellToolConfig{}), - "PostEditConfig": reflect.TypeOf(PostEditConfig{}), - "PermissionsConfig": reflect.TypeOf(PermissionsConfig{}), - "HooksConfig": reflect.TypeOf(HooksConfig{}), - "HookMatcherConfig": reflect.TypeOf(HookMatcherConfig{}), - "HookDefinition": reflect.TypeOf(HookDefinition{}), - "RoutingRule": reflect.TypeOf(RoutingRule{}), - "ApiConfig": reflect.TypeOf(APIToolConfig{}), + "AgentConfig": reflect.TypeFor[AgentConfig](), + "FallbackConfig": reflect.TypeFor[FallbackConfig](), + "ModelConfig": reflect.TypeFor[ModelConfig](), + "Metadata": reflect.TypeFor[Metadata](), + "ProviderConfig": reflect.TypeFor[ProviderConfig](), + "Toolset": reflect.TypeFor[Toolset](), + "Remote": reflect.TypeFor[Remote](), + "SandboxConfig": reflect.TypeFor[SandboxConfig](), + "ScriptShellToolConfig": reflect.TypeFor[ScriptShellToolConfig](), + "PostEditConfig": reflect.TypeFor[PostEditConfig](), + "PermissionsConfig": reflect.TypeFor[PermissionsConfig](), + "HooksConfig": reflect.TypeFor[HooksConfig](), + "HookMatcherConfig": reflect.TypeFor[HookMatcherConfig](), + "HookDefinition": reflect.TypeFor[HookDefinition](), + "RoutingRule": reflect.TypeFor[RoutingRule](), + "ApiConfig": reflect.TypeFor[APIToolConfig](), } for name, goType := range definitionMap { @@ -159,13 +156,13 @@ func TestSchemaMatchesGoTypes(t *testing.T) { } inlines := []inlineEntry{ - {reflect.TypeOf(StructuredOutput{}), []string{"AgentConfig", "structured_output"}, "StructuredOutput (AgentConfig.structured_output)"}, - {reflect.TypeOf(RAGConfig{}), []string{"RAGConfig"}, "RAGConfig"}, - {reflect.TypeOf(RAGToolConfig{}), []string{"RAGConfig", "tool"}, "RAGToolConfig (RAGConfig.tool)"}, - {reflect.TypeOf(RAGResultsConfig{}), []string{"RAGConfig", "results"}, "RAGResultsConfig (RAGConfig.results)"}, - {reflect.TypeOf(RAGFusionConfig{}), []string{"RAGConfig", "results", "fusion"}, "RAGFusionConfig (RAGConfig.results.fusion)"}, - {reflect.TypeOf(RAGRerankingConfig{}), []string{"RAGConfig", "results", "reranking"}, "RAGRerankingConfig (RAGConfig.results.reranking)"}, - {reflect.TypeOf(RAGChunkingConfig{}), []string{"RAGConfig", "strategies", "*", "chunking"}, "RAGChunkingConfig (RAGConfig.strategies[].chunking)"}, + {reflect.TypeFor[StructuredOutput](), []string{"AgentConfig", "structured_output"}, "StructuredOutput (AgentConfig.structured_output)"}, + {reflect.TypeFor[RAGConfig](), []string{"RAGConfig"}, "RAGConfig"}, + {reflect.TypeFor[RAGToolConfig](), []string{"RAGConfig", "tool"}, "RAGToolConfig (RAGConfig.tool)"}, + {reflect.TypeFor[RAGResultsConfig](), []string{"RAGConfig", "results"}, "RAGResultsConfig (RAGConfig.results)"}, + {reflect.TypeFor[RAGFusionConfig](), []string{"RAGConfig", "results", "fusion"}, "RAGFusionConfig (RAGConfig.results.fusion)"}, + {reflect.TypeFor[RAGRerankingConfig](), []string{"RAGConfig", "results", "reranking"}, "RAGRerankingConfig (RAGConfig.results.reranking)"}, + {reflect.TypeFor[RAGChunkingConfig](), []string{"RAGConfig", "strategies", "*", "chunking"}, "RAGChunkingConfig (RAGConfig.strategies[].chunking)"}, } for _, il := range inlines { diff --git a/pkg/config/v4/types.go b/pkg/config/v4/types.go index 087a90984..8a94fb7ec 100644 --- a/pkg/config/v4/types.go +++ b/pkg/config/v4/types.go @@ -3,6 +3,7 @@ package v4 import ( "encoding/json" "fmt" + "maps" "strings" "time" @@ -20,7 +21,7 @@ type Config struct { Providers map[string]ProviderConfig `json:"providers,omitempty"` Models map[string]ModelConfig `json:"models,omitempty"` RAG map[string]RAGConfig `json:"rag,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` + Metadata Metadata `json:"metadata"` Permissions *PermissionsConfig `json:"permissions,omitempty"` } @@ -125,7 +126,7 @@ type FallbackConfig struct { // Cooldown is the duration to stick with a successful fallback model before // retrying the primary. Only applies after a non-retryable error (e.g., 429). // Default is 1 minute. Use Go duration format (e.g., "1m", "30s", "2m30s"). - Cooldown Duration `json:"cooldown,omitempty"` + Cooldown Duration `json:"cooldown"` } // Duration is a wrapper around time.Duration that supports YAML/JSON unmarshaling @@ -410,13 +411,13 @@ type Toolset struct { Instruction string `json:"instruction,omitempty"` Toon string `json:"toon,omitempty"` - Defer DeferConfig `json:"defer,omitempty" yaml:"defer,omitempty"` + Defer DeferConfig `json:"defer" yaml:"defer,omitempty"` // For the `mcp` tool Command string `json:"command,omitempty"` Args []string `json:"args,omitempty"` Ref string `json:"ref,omitempty"` - Remote Remote `json:"remote,omitempty"` + Remote Remote `json:"remote"` Config any `json:"config,omitempty"` // For the `a2a` tool @@ -441,7 +442,7 @@ type Toolset struct { // For the `filesystem` tool - post-edit commands PostEdit []PostEditConfig `json:"post_edit,omitempty"` - APIConfig APIToolConfig `json:"api_config,omitempty"` + APIConfig APIToolConfig `json:"api_config"` // For the `filesystem` tool - VCS integration IgnoreVCS *bool `json:"ignore_vcs,omitempty"` @@ -569,11 +570,11 @@ func (t ThinkingBudget) MarshalYAML() ([]byte, error) { func (t ThinkingBudget) MarshalJSON() ([]byte, error) { // If Effort string is set (non-empty), marshal as string if t.Effort != "" { - return []byte(fmt.Sprintf("%q", t.Effort)), nil + return fmt.Appendf(nil, "%q", t.Effort), nil } // Otherwise marshal as integer (includes 0, -1, and positive values) - return []byte(fmt.Sprintf("%d", t.Tokens)), nil + return fmt.Appendf(nil, "%d", t.Tokens), nil } // UnmarshalJSON implements custom unmarshaling to accept simple string or int format @@ -618,11 +619,11 @@ type RAGToolConfig struct { // RAGConfig represents a RAG (Retrieval-Augmented Generation) configuration // Uses a unified strategies array for flexible, extensible configuration type RAGConfig struct { - Tool RAGToolConfig `json:"tool,omitempty"` // Tool configuration + Tool RAGToolConfig `json:"tool"` // Tool configuration Docs []string `json:"docs,omitempty"` // Shared documents across all strategies RespectVCS *bool `json:"respect_vcs,omitempty"` // Whether to respect VCS ignore files like .gitignore (default: true) Strategies []RAGStrategyConfig `json:"strategies,omitempty"` // Array of strategy configurations - Results RAGResultsConfig `json:"results,omitempty"` + Results RAGResultsConfig `json:"results"` } // GetRespectVCS returns whether VCS ignore files should be respected, defaulting to true @@ -636,11 +637,11 @@ func (c *RAGConfig) GetRespectVCS() bool { // RAGStrategyConfig represents a single retrieval strategy configuration // Strategy-specific fields are stored in Params (validated by strategy implementation) type RAGStrategyConfig struct { //nolint:recvcheck // Marshal methods must use value receiver for YAML/JSON slice encoding, Unmarshal must use pointer - Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc. - Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs) - Database RAGDatabaseConfig `json:"database,omitempty"` // Database configuration - Chunking RAGChunkingConfig `json:"chunking,omitempty"` // Chunking configuration - Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input) + Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc. + Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs) + Database RAGDatabaseConfig `json:"database"` // Database configuration + Chunking RAGChunkingConfig `json:"chunking"` // Chunking configuration + Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input) // Strategy-specific parameters (arbitrary key-value pairs) // Examples: @@ -793,9 +794,7 @@ func (s RAGStrategyConfig) buildFlattenedMap() map[string]any { } // Flatten Params into the same level - for k, v := range s.Params { - result[k] = v - } + maps.Copy(result, s.Params) return result } diff --git a/pkg/evaluation/build.go b/pkg/evaluation/build.go index 08e5b4265..a5b1d11e8 100644 --- a/pkg/evaluation/build.go +++ b/pkg/evaluation/build.go @@ -82,8 +82,7 @@ func (r *Runner) buildEvalImage(ctx context.Context, workingDir string) (string, output, err := cmd.Output() if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { + if exitErr, ok := errors.AsType[*exec.ExitError](err); ok { return "", fmt.Errorf("docker build failed: %s", string(exitErr.Stderr)) } return "", fmt.Errorf("docker build failed: %w", err) diff --git a/pkg/evaluation/judge.go b/pkg/evaluation/judge.go index 4d003cc80..e00401dbe 100644 --- a/pkg/evaluation/judge.go +++ b/pkg/evaluation/judge.go @@ -106,9 +106,7 @@ func (j *Judge) CheckRelevance(ctx context.Context, response string, criteria [] var wg sync.WaitGroup for range j.concurrency { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for item := range work { if ctx.Err() != nil { results[item.index] = result{err: fmt.Errorf("context cancelled: %w", ctx.Err())} @@ -117,7 +115,7 @@ func (j *Judge) CheckRelevance(ctx context.Context, response string, criteria [] pass, reason, err := j.checkSingle(ctx, response, item.criterion) results[item.index] = result{passed: pass, reason: reason, err: err} } - }() + }) } wg.Wait() diff --git a/pkg/gateway/types.go b/pkg/gateway/types.go index 51c1dbe24..526dfff7b 100644 --- a/pkg/gateway/types.go +++ b/pkg/gateway/types.go @@ -9,7 +9,7 @@ type Catalog map[string]Server type Server struct { Type string `json:"type"` Secrets []Secret `json:"secrets,omitempty"` - Remote Remote `json:"remote,omitempty"` + Remote Remote `json:"remote"` } type Remote struct { diff --git a/pkg/hooks/executor.go b/pkg/hooks/executor.go index 7d86c2027..a673a32e3 100644 --- a/pkg/hooks/executor.go +++ b/pkg/hooks/executor.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "log/slog" + "maps" "os" "os/exec" "regexp" @@ -218,18 +219,16 @@ func (e *Executor) executeHooks(ctx context.Context, hooks []Hook, input *Input, var wg sync.WaitGroup for i, hook := range uniqueHooks { - wg.Add(1) - go func(idx int, h Hook) { - defer wg.Done() - output, stdout, stderr, exitCode, err := e.executeHook(ctx, h, inputJSON) - results[idx] = hookResult{ + wg.Go(func() { + output, stdout, stderr, exitCode, err := e.executeHook(ctx, hook, inputJSON) + results[i] = hookResult{ output: output, stdout: stdout, stderr: stderr, exitCode: exitCode, err: err, } - }(i, hook) + }) } wg.Wait() @@ -265,8 +264,7 @@ func (e *Executor) executeHook(ctx context.Context, hook Hook, inputJSON []byte) exitCode := 0 if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { + if exitErr, ok := errors.AsType[*exec.ExitError](err); ok { exitCode = exitErr.ExitCode() } else { return nil, stdout.String(), stderr.String(), -1, err @@ -367,9 +365,7 @@ func (e *Executor) aggregateResults(results []hookResult, eventType EventType) ( if finalResult.ModifiedInput == nil { finalResult.ModifiedInput = make(map[string]any) } - for k, v := range hso.UpdatedInput { - finalResult.ModifiedInput[k] = v - } + maps.Copy(finalResult.ModifiedInput, hso.UpdatedInput) } } diff --git a/pkg/hooks/hooks_test.go b/pkg/hooks/hooks_test.go index 922135dfb..7166bd67d 100644 --- a/pkg/hooks/hooks_test.go +++ b/pkg/hooks/hooks_test.go @@ -143,12 +143,12 @@ func TestOutputShouldContinue(t *testing.T) { }, { name: "continue true", - output: Output{Continue: ptrBool(true)}, + output: Output{Continue: new(true)}, expected: true, }, { name: "continue false", - output: Output{Continue: ptrBool(false)}, + output: Output{Continue: new(false)}, expected: false, }, } @@ -509,7 +509,3 @@ func TestExecuteHooksWithContextCancellation(t *testing.T) { // Should be allowed because the hook timed out (non-blocking error) assert.True(t, result.Allowed) } - -func ptrBool(b bool) *bool { - return &b -} diff --git a/pkg/model/provider/anthropic/adapter.go b/pkg/model/provider/anthropic/adapter.go index 581503f74..68ef93213 100644 --- a/pkg/model/provider/anthropic/adapter.go +++ b/pkg/model/provider/anthropic/adapter.go @@ -45,8 +45,8 @@ func isContextLengthError(err error) bool { return false } - var apiErr *anthropic.Error - if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusBadRequest { + apiErr, ok := errors.AsType[*anthropic.Error](err) + if !ok || apiErr.StatusCode != http.StatusBadRequest { return false } diff --git a/pkg/model/provider/anthropic/files.go b/pkg/model/provider/anthropic/files.go index 46bc14f65..299f6c63d 100644 --- a/pkg/model/provider/anthropic/files.go +++ b/pkg/model/provider/anthropic/files.go @@ -10,6 +10,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "sync" "time" @@ -298,11 +299,8 @@ func (fm *FileManager) Cleanup(ctx context.Context, ttl time.Duration) error { // Collect paths to delete var pathsToDelete []string for path, k := range fm.paths { - for _, deletedKey := range keysToDelete { - if k == deletedKey { - pathsToDelete = append(pathsToDelete, path) - break - } + if slices.Contains(keysToDelete, k) { + pathsToDelete = append(pathsToDelete, path) } } diff --git a/pkg/model/provider/dmr/client.go b/pkg/model/provider/dmr/client.go index d7a8f7346..f93e7ba2f 100644 --- a/pkg/model/provider/dmr/client.go +++ b/pkg/model/provider/dmr/client.go @@ -691,7 +691,7 @@ func (c *Client) Rerank(ctx context.Context, query string, documents []types.Doc Usage struct { PromptTokens int `json:"prompt_tokens"` TotalTokens int `json:"total_tokens"` - } `json:"usage,omitempty"` + } `json:"usage"` } reqBody := rerankRequest{ diff --git a/pkg/model/provider/dmr/client_test.go b/pkg/model/provider/dmr/client_test.go index b77ca0bd8..5bfb2d297 100644 --- a/pkg/model/provider/dmr/client_test.go +++ b/pkg/model/provider/dmr/client_test.go @@ -341,10 +341,10 @@ func TestBuildRuntimeFlagsFromModelConfig_LlamaCpp(t *testing.T) { t.Parallel() flags := buildRuntimeFlagsFromModelConfig("llama.cpp", &latest.ModelConfig{ - Temperature: floatPtr(0.6), - TopP: floatPtr(0.95), - FrequencyPenalty: floatPtr(0.2), - PresencePenalty: floatPtr(0.1), + Temperature: new(0.6), + TopP: new(0.95), + FrequencyPenalty: new(0.2), + PresencePenalty: new(0.1), }) assert.Equal(t, []string{"--temp", "0.6", "--top-p", "0.95", "--frequency-penalty", "0.2", "--presence-penalty", "0.1"}, flags) @@ -354,9 +354,9 @@ func TestIntegrateFlagsWithProviderOptsOrder(t *testing.T) { t.Parallel() cfg := &latest.ModelConfig{ - Temperature: floatPtr(0.6), - TopP: floatPtr(0.9), - MaxTokens: int64Ptr(4096), + Temperature: new(0.6), + TopP: new(0.9), + MaxTokens: new(int64(4096)), ProviderOpts: map[string]any{ "runtime_flags": []string{"--threads", "6"}, }, @@ -389,19 +389,11 @@ func TestMergeRuntimeFlagsPreferUser_WarnsAndPrefersUser(t *testing.T) { assert.Equal(t, []string{"--top-p", "0.8", "--temp", "0.7", "--threads", "8"}, merged) } -func floatPtr(f float64) *float64 { - return &f -} - -func int64Ptr(i int64) *int64 { - return &i -} - func TestParseDMRProviderOptsWithSpeculativeDecoding(t *testing.T) { t.Parallel() cfg := &latest.ModelConfig{ - MaxTokens: int64Ptr(4096), + MaxTokens: new(int64(4096)), ProviderOpts: map[string]any{ "speculative_draft_model": "ai/qwen3:1B", "speculative_num_tokens": "5", @@ -424,7 +416,7 @@ func TestParseDMRProviderOptsWithoutSpeculativeDecoding(t *testing.T) { t.Parallel() cfg := &latest.ModelConfig{ - MaxTokens: int64Ptr(4096), + MaxTokens: new(int64(4096)), ProviderOpts: map[string]any{ "runtime_flags": []string{"--threads", "8"}, }, diff --git a/pkg/model/provider/gemini/client.go b/pkg/model/provider/gemini/client.go index 2527218c5..02e6b0d55 100644 --- a/pkg/model/provider/gemini/client.go +++ b/pkg/model/provider/gemini/client.go @@ -296,16 +296,16 @@ func (c *Client) buildConfig() *genai.GenerateContentConfig { config.MaxOutputTokens = int32(*c.ModelConfig.MaxTokens) } if c.ModelConfig.Temperature != nil { - config.Temperature = genai.Ptr(float32(*c.ModelConfig.Temperature)) + config.Temperature = new(float32(*c.ModelConfig.Temperature)) } if c.ModelConfig.TopP != nil { - config.TopP = genai.Ptr(float32(*c.ModelConfig.TopP)) + config.TopP = new(float32(*c.ModelConfig.TopP)) } if c.ModelConfig.FrequencyPenalty != nil { - config.FrequencyPenalty = genai.Ptr(float32(*c.ModelConfig.FrequencyPenalty)) + config.FrequencyPenalty = new(float32(*c.ModelConfig.FrequencyPenalty)) } if c.ModelConfig.PresencePenalty != nil { - config.PresencePenalty = genai.Ptr(float32(*c.ModelConfig.PresencePenalty)) + config.PresencePenalty = new(float32(*c.ModelConfig.PresencePenalty)) } // Apply thinking configuration for Gemini models. @@ -349,7 +349,7 @@ func (c *Client) buildConfig() *genai.GenerateContentConfig { // Gemini 2.5 and older: ThinkingBudget=0 disables thinking. config.ThinkingConfig = &genai.ThinkingConfig{ IncludeThoughts: false, - ThinkingBudget: genai.Ptr(int32(0)), + ThinkingBudget: new(int32(0)), } slog.Debug("Gemini thinking explicitly disabled via ModelOptions", "model", c.ModelConfig.Model, @@ -437,7 +437,7 @@ func (c *Client) applyGemini3ThinkingLevel(config *genai.GenerateContentConfig) // applyGemini25ThinkingBudget applies token-based thinking for Gemini 2.5 and other models. func (c *Client) applyGemini25ThinkingBudget(config *genai.GenerateContentConfig) { tokens := c.ModelConfig.ThinkingBudget.Tokens - config.ThinkingConfig.ThinkingBudget = genai.Ptr(int32(tokens)) + config.ThinkingConfig.ThinkingBudget = new(int32(tokens)) switch tokens { case 0: @@ -650,7 +650,7 @@ func (c *Client) Rerank(ctx context.Context, query string, documents []types.Doc // For reranking, default temperature to 0 for deterministic scoring if not explicitly set. if c.ModelConfig.Temperature == nil { - cfg.Temperature = genai.Ptr(float32(0.0)) + cfg.Temperature = new(float32(0.0)) } // Disable thinking for reranking - we want quick, deterministic scoring diff --git a/pkg/model/provider/gemini/client_test.go b/pkg/model/provider/gemini/client_test.go index 7c476ec35..1992fe5d5 100644 --- a/pkg/model/provider/gemini/client_test.go +++ b/pkg/model/provider/gemini/client_test.go @@ -26,35 +26,35 @@ func TestBuildConfig_Gemini25_ThinkingBudget(t *testing.T) { name: "gemini-2.5-flash with dynamic thinking (-1)", model: "gemini-2.5-flash", thinkingBudget: &latest.ThinkingBudget{Tokens: -1}, - expectThinkingBudget: ptr(int32(-1)), + expectThinkingBudget: new(int32(-1)), expectThinkingLevel: "", }, { name: "gemini-2.5-pro with dynamic thinking (-1)", model: "gemini-2.5-pro", thinkingBudget: &latest.ThinkingBudget{Tokens: -1}, - expectThinkingBudget: ptr(int32(-1)), + expectThinkingBudget: new(int32(-1)), expectThinkingLevel: "", }, { name: "gemini-2.5-flash with specific token budget", model: "gemini-2.5-flash", thinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, - expectThinkingBudget: ptr(int32(8192)), + expectThinkingBudget: new(int32(8192)), expectThinkingLevel: "", }, { name: "gemini-2.5-flash with thinking disabled (0)", model: "gemini-2.5-flash", thinkingBudget: &latest.ThinkingBudget{Tokens: 0}, - expectThinkingBudget: ptr(int32(0)), + expectThinkingBudget: new(int32(0)), expectThinkingLevel: "", }, { name: "gemini-2.5-flash-lite with dynamic thinking", model: "gemini-2.5-flash-lite", thinkingBudget: &latest.ThinkingBudget{Tokens: -1}, - expectThinkingBudget: ptr(int32(-1)), + expectThinkingBudget: new(int32(-1)), expectThinkingLevel: "", }, } @@ -406,8 +406,3 @@ func TestBuildConfig_ThinkingNotSet(t *testing.T) { assert.True(t, config.ThinkingConfig.IncludeThoughts, "IncludeThoughts should be true") assert.Equal(t, genai.ThinkingLevelHigh, config.ThinkingConfig.ThinkingLevel, "ThinkingLevel should match ThinkingBudget") } - -// ptr is a helper to create a pointer to an int32 value. -func ptr(v int32) *int32 { - return &v -} diff --git a/pkg/model/provider/model_defaults_test.go b/pkg/model/provider/model_defaults_test.go index a2854b48b..28ad89278 100644 --- a/pkg/model/provider/model_defaults_test.go +++ b/pkg/model/provider/model_defaults_test.go @@ -1,6 +1,7 @@ package provider import ( + "maps" "testing" "github.com/stretchr/testify/assert" @@ -180,9 +181,7 @@ func TestApplyModelDefaults_Anthropic(t *testing.T) { // Save original ProviderOpts keys to check preservation originalOpts := make(map[string]any) if tt.config.ProviderOpts != nil { - for k, v := range tt.config.ProviderOpts { - originalOpts[k] = v - } + maps.Copy(originalOpts, tt.config.ProviderOpts) } // Apply defaults @@ -451,9 +450,7 @@ func TestApplyModelDefaults_Bedrock(t *testing.T) { // Save original ProviderOpts keys to check preservation originalOpts := make(map[string]any) if tt.config.ProviderOpts != nil { - for k, v := range tt.config.ProviderOpts { - originalOpts[k] = v - } + maps.Copy(originalOpts, tt.config.ProviderOpts) } // Apply defaults @@ -574,7 +571,7 @@ func TestApplyProviderDefaults_IncludesModelDefaults(t *testing.T) { Model: "claude-sonnet-4-0", }, expectThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, - expectInterleavedThinking: boolPtr(true), + expectInterleavedThinking: new(true), }, { name: "google gemini-2.5 model gets defaults", @@ -607,7 +604,7 @@ func TestApplyProviderDefaults_IncludesModelDefaults(t *testing.T) { Model: "anthropic.claude-3-sonnet", }, expectThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, - expectInterleavedThinking: boolPtr(true), + expectInterleavedThinking: new(true), }, { name: "bedrock global claude model gets defaults", @@ -616,7 +613,7 @@ func TestApplyProviderDefaults_IncludesModelDefaults(t *testing.T) { Model: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", }, expectThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, - expectInterleavedThinking: boolPtr(true), + expectInterleavedThinking: new(true), }, { name: "custom provider with openai api_type gets openai defaults", @@ -644,7 +641,7 @@ func TestApplyProviderDefaults_IncludesModelDefaults(t *testing.T) { }, customProviders: nil, expectThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, - expectInterleavedThinking: boolPtr(true), + expectInterleavedThinking: new(true), }, } @@ -672,11 +669,6 @@ func TestApplyProviderDefaults_IncludesModelDefaults(t *testing.T) { } } -// boolPtr is a helper to create a pointer to a bool value. -func boolPtr(b bool) *bool { - return &b -} - // TestApplyProviderDefaults_ThinkingDefaultsApplied tests that thinking defaults // are always applied when the config doesn't have an explicit thinking budget. func TestApplyProviderDefaults_ThinkingDefaultsApplied(t *testing.T) { diff --git a/pkg/model/provider/override_test.go b/pkg/model/provider/override_test.go index 0769fd737..4408cc468 100644 --- a/pkg/model/provider/override_test.go +++ b/pkg/model/provider/override_test.go @@ -29,7 +29,7 @@ func TestApplyOverrides_Thinking(t *testing.T) { Model: "claude-sonnet-4-0", ThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, }, - thinkingEnabled: boolPtr(false), + thinkingEnabled: new(false), expectThinkingBudget: nil, }, { @@ -40,7 +40,7 @@ func TestApplyOverrides_Thinking(t *testing.T) { ThinkingBudget: &latest.ThinkingBudget{Tokens: 16384}, ProviderOpts: map[string]any{"interleaved_thinking": true}, }, - thinkingEnabled: boolPtr(false), + thinkingEnabled: new(false), expectThinkingBudget: nil, expectInterleavedThinking: nil, // key should be removed }, @@ -51,7 +51,7 @@ func TestApplyOverrides_Thinking(t *testing.T) { Model: "claude-sonnet-4-0", ThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, }, - thinkingEnabled: boolPtr(true), + thinkingEnabled: new(true), expectThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, }, { @@ -62,9 +62,9 @@ func TestApplyOverrides_Thinking(t *testing.T) { ThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, ProviderOpts: map[string]any{"interleaved_thinking": true}, }, - thinkingEnabled: boolPtr(true), + thinkingEnabled: new(true), expectThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, - expectInterleavedThinking: boolPtr(true), + expectInterleavedThinking: new(true), }, { name: "preserves other ProviderOpts when clearing thinking", @@ -77,7 +77,7 @@ func TestApplyOverrides_Thinking(t *testing.T) { "other_option": "preserved", }, }, - thinkingEnabled: boolPtr(false), + thinkingEnabled: new(false), expectThinkingBudget: nil, }, { @@ -97,7 +97,7 @@ func TestApplyOverrides_Thinking(t *testing.T) { Model: "gpt-4o", ThinkingBudget: nil, // No thinking configured }, - thinkingEnabled: boolPtr(true), + thinkingEnabled: new(true), expectThinkingBudget: &latest.ThinkingBudget{Effort: "medium"}, // OpenAI default }, { @@ -107,9 +107,9 @@ func TestApplyOverrides_Thinking(t *testing.T) { Model: "claude-sonnet-4-0", ThinkingBudget: nil, // No thinking configured }, - thinkingEnabled: boolPtr(true), + thinkingEnabled: new(true), expectThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, // Anthropic default - expectInterleavedThinking: boolPtr(true), // Anthropic default + expectInterleavedThinking: new(true), // Anthropic default }, { name: "restores defaults when /think used with tokens=0", @@ -118,7 +118,7 @@ func TestApplyOverrides_Thinking(t *testing.T) { Model: "gpt-4o", ThinkingBudget: &latest.ThinkingBudget{Tokens: 0}, // User had thinking disabled }, - thinkingEnabled: boolPtr(true), // User runs /think + thinkingEnabled: new(true), // User runs /think expectThinkingBudget: &latest.ThinkingBudget{Effort: "medium"}, // Apply OpenAI default }, { @@ -128,9 +128,9 @@ func TestApplyOverrides_Thinking(t *testing.T) { Model: "claude-sonnet-4-0", ThinkingBudget: &latest.ThinkingBudget{Effort: "none"}, // User had thinking disabled }, - thinkingEnabled: boolPtr(true), // User runs /think + thinkingEnabled: new(true), // User runs /think expectThinkingBudget: &latest.ThinkingBudget{Tokens: 8192}, // Apply Anthropic default - expectInterleavedThinking: boolPtr(true), + expectInterleavedThinking: new(true), }, } diff --git a/pkg/model/provider/provider.go b/pkg/model/provider/provider.go index 504a0811b..987a86b99 100644 --- a/pkg/model/provider/provider.go +++ b/pkg/model/provider/provider.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "log/slog" + "maps" + "slices" "strings" "github.com/docker/cagent/pkg/chat" @@ -60,10 +62,8 @@ func CatalogProviders() []string { // IsCatalogProvider returns true if the provider name is valid for the model catalog. func IsCatalogProvider(name string) bool { // Check core providers - for _, p := range CoreProviders { - if p == name { - return true - } + if slices.Contains(CoreProviders, name) { + return true } // Check aliases with BaseURL if alias, exists := Aliases[name]; exists && alias.BaseURL != "" { @@ -218,9 +218,7 @@ func createDirectProvider(ctx context.Context, cfg *latest.ModelConfig, env envi if enhancedCfg.ProviderOpts != nil { // Copy to avoid mutating shared ProviderOpts in the original config optsCopy := make(map[string]any, len(enhancedCfg.ProviderOpts)) - for key, value := range enhancedCfg.ProviderOpts { - optsCopy[key] = value - } + maps.Copy(optsCopy, enhancedCfg.ProviderOpts) delete(optsCopy, "interleaved_thinking") enhancedCfg.ProviderOpts = optsCopy } diff --git a/pkg/modelsdev/store.go b/pkg/modelsdev/store.go index 9423e9b69..b8475d12f 100644 --- a/pkg/modelsdev/store.go +++ b/pkg/modelsdev/store.go @@ -183,10 +183,10 @@ func (s *Store) GetModel(ctx context.Context, id string) (*Model, error) { // // Strip known region prefixes and retry lookup. if providerID == "amazon-bedrock" { - if idx := strings.Index(modelID, "."); idx != -1 { - possibleRegionPrefix := modelID[:idx] + if before, after, ok := strings.Cut(modelID, "."); ok { + possibleRegionPrefix := before if isBedrockRegionPrefix(possibleRegionPrefix) { - normalizedModelID := modelID[idx+1:] + normalizedModelID := after model, exists = provider.Models[normalizedModelID] if exists { return &model, nil diff --git a/pkg/rag/treesitter/treesitter.go b/pkg/rag/treesitter/treesitter.go index d4d6b83fa..15b4f2dae 100644 --- a/pkg/rag/treesitter/treesitter.go +++ b/pkg/rag/treesitter/treesitter.go @@ -518,8 +518,8 @@ func extractPackageName(root *sitter.Node, content []byte) string { scanner := bufio.NewScanner(bytes.NewReader(content)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) - if strings.HasPrefix(line, "package ") { - return strings.TrimSpace(strings.TrimPrefix(line, "package ")) + if after, ok := strings.CutPrefix(line, "package "); ok { + return strings.TrimSpace(after) } } diff --git a/pkg/runtime/fallback.go b/pkg/runtime/fallback.go index f641f1af2..863d666b1 100644 --- a/pkg/runtime/fallback.go +++ b/pkg/runtime/fallback.go @@ -63,14 +63,12 @@ func extractHTTPStatusCode(err error) int { } // Check Anthropic SDK error type (public) - var anthropicErr *anthropic.Error - if errors.As(err, &anthropicErr) { + if anthropicErr, ok := errors.AsType[*anthropic.Error](err); ok { return anthropicErr.StatusCode } // Check Google Gemini SDK error type (public) - var geminiErr *genai.APIError - if errors.As(err, &geminiErr) { + if geminiErr, ok := errors.AsType[*genai.APIError](err); ok { return geminiErr.Code } @@ -152,8 +150,7 @@ func isRetryableModelError(err error) bool { } // Check for network errors - var netErr net.Error - if errors.As(err, &netErr) { + if netErr, ok := errors.AsType[net.Error](err); ok { // Timeout errors are retryable if netErr.Timeout() { slog.Debug("Network timeout error, retryable", "error", err) diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index a1c1250ca..344b987d3 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -238,7 +238,7 @@ func clearTimestamps(event Event) { // Use reflection to find and clear Timestamp in embedded AgentContext v := reflect.ValueOf(event) - if v.Kind() == reflect.Ptr { + if v.Kind() == reflect.Pointer { v = v.Elem() } if v.Kind() != reflect.Struct { diff --git a/pkg/session/branch.go b/pkg/session/branch.go index 11b947471..55d24c9c6 100644 --- a/pkg/session/branch.go +++ b/pkg/session/branch.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "maps" "strings" "time" @@ -144,9 +145,7 @@ func cloneStringMap(src map[string]string) map[string]string { return nil } dst := make(map[string]string, len(src)) - for key, val := range src { - dst[key] = val - } + maps.Copy(dst, src) return dst } diff --git a/pkg/sessiontitle/generator.go b/pkg/sessiontitle/generator.go index 9b92ee720..3e9f34b0a 100644 --- a/pkg/sessiontitle/generator.go +++ b/pkg/sessiontitle/generator.go @@ -173,8 +173,8 @@ func (g *Generator) Generate(ctx context.Context, sessionID string, userMessages // non-empty line and stripping any control characters that could break TUI rendering. func sanitizeTitle(title string) string { // Split by newlines and take the first non-empty line - lines := strings.Split(title, "\n") - for _, line := range lines { + lines := strings.SplitSeq(title, "\n") + for line := range lines { line = strings.TrimSpace(line) if line != "" { // Remove any remaining carriage returns diff --git a/pkg/sqliteutil/sqlite.go b/pkg/sqliteutil/sqlite.go index 90616fbd3..fcfa41b14 100644 --- a/pkg/sqliteutil/sqlite.go +++ b/pkg/sqliteutil/sqlite.go @@ -53,8 +53,7 @@ func OpenDB(path string) (*sql.DB, error) { // IsCantOpenError checks if the error is a SQLite CANTOPEN error (code 14). func IsCantOpenError(err error) bool { - var sqliteErr *sqlite.Error - if errors.As(err, &sqliteErr) { + if sqliteErr, ok := errors.AsType[*sqlite.Error](err); ok { return sqliteErr.Code() == sqlite3.SQLITE_CANTOPEN } return false @@ -63,8 +62,7 @@ func IsCantOpenError(err error) bool { // IsNoSuchColumnError checks if the error is due to a missing column in SQLite. // This typically happens when querying a column that doesn't exist in the schema. func IsNoSuchColumnError(err error) bool { - var sqliteErr *sqlite.Error - if errors.As(err, &sqliteErr) { + if sqliteErr, ok := errors.AsType[*sqlite.Error](err); ok { // SQLITE_ERROR (1) is the generic SQL error code used for "no such column" return sqliteErr.Code() == sqlite3.SQLITE_ERROR } diff --git a/pkg/teamloader/teamloader.go b/pkg/teamloader/teamloader.go index 0200ed7b5..7828209ef 100644 --- a/pkg/teamloader/teamloader.go +++ b/pkg/teamloader/teamloader.go @@ -187,8 +187,7 @@ func LoadWithConfig(ctx context.Context, agentSource config.Source, runConfig *c if err != nil { // Return auto model fallback errors and DMR not installed errors directly // without wrapping to provide cleaner messages - var autoErr *config.AutoModelFallbackError - if errors.As(err, &autoErr) || errors.Is(err, dmr.ErrNotInstalled) { + if _, ok := errors.AsType[*config.AutoModelFallbackError](err); ok || errors.Is(err, dmr.ErrNotInstalled) { return nil, err } return nil, fmt.Errorf("failed to get models: %w", err) diff --git a/pkg/telemetry/http.go b/pkg/telemetry/http.go index 9526e4a9d..3cbaebc88 100644 --- a/pkg/telemetry/http.go +++ b/pkg/telemetry/http.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "log/slog" + "maps" "net/http" "time" ) @@ -18,9 +19,7 @@ func (tc *Client) createEvent(eventName string, properties map[string]any) Event allProperties := make(map[string]any) // Copy user-provided properties first - for k, v := range properties { - allProperties[k] = v - } + maps.Copy(allProperties, properties) // Add system metadata to properties allProperties["user_uuid"] = tc.userUUID diff --git a/pkg/tools/builtin/lsp.go b/pkg/tools/builtin/lsp.go index ba52b5dfd..195302bd5 100644 --- a/pkg/tools/builtin/lsp.go +++ b/pkg/tools/builtin/lsp.go @@ -851,8 +851,7 @@ func (h *lspHandler) stop(_ context.Context) error { h.openFilesMu.Unlock() if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { + if _, ok := errors.AsType[*exec.ExitError](err); ok { return nil } return fmt.Errorf("LSP server exited with error: %w", err) @@ -1718,8 +1717,8 @@ func (h *lspHandler) readMessageLocked() ([]byte, error) { if line == "" { break } - if strings.HasPrefix(line, "Content-Length:") { - lengthStr := strings.TrimSpace(strings.TrimPrefix(line, "Content-Length:")) + if after, ok := strings.CutPrefix(line, "Content-Length:"); ok { + lengthStr := strings.TrimSpace(after) contentLength, err = strconv.Atoi(lengthStr) if err != nil { return nil, fmt.Errorf("invalid Content-Length: %w", err) diff --git a/pkg/tools/builtin/shell.go b/pkg/tools/builtin/shell.go index 88735a843..91978074d 100644 --- a/pkg/tools/builtin/shell.go +++ b/pkg/tools/builtin/shell.go @@ -243,8 +243,7 @@ func (h *shellHandler) monitorJob(job *backgroundJob, cmd *exec.Cmd) { } if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { + if exitErr, ok := errors.AsType[*exec.ExitError](err); ok { job.exitCode = exitErr.ExitCode() } else { job.exitCode = -1 diff --git a/pkg/tools/mcp/tokenstore.go b/pkg/tools/mcp/tokenstore.go index b024167f5..ca530b11a 100644 --- a/pkg/tools/mcp/tokenstore.go +++ b/pkg/tools/mcp/tokenstore.go @@ -23,7 +23,7 @@ type OAuthToken struct { ExpiresIn int `json:"expires_in,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` Scope string `json:"scope,omitempty"` - ExpiresAt time.Time `json:"expires_at,omitempty"` + ExpiresAt time.Time `json:"expires_at"` } // IsExpired checks if the token is expired diff --git a/pkg/tui/components/markdown/fast_renderer.go b/pkg/tui/components/markdown/fast_renderer.go index f58bc7cb7..7ed28fbe1 100644 --- a/pkg/tui/components/markdown/fast_renderer.go +++ b/pkg/tui/components/markdown/fast_renderer.go @@ -546,10 +546,9 @@ func (p *parser) renderBlockquoteContent(lines []string, indent string, availabl // Render the nested blockquote with additional indentation nestedIndent := indent + spaces(p.styles.blockquoteIndent) - nestedWidth := availableWidth - p.styles.blockquoteIndent - if nestedWidth < 10 { - nestedWidth = 10 // Minimum content width - } + nestedWidth := max(availableWidth-p.styles.blockquoteIndent, + // Minimum content width + 10) p.renderBlockquoteContent(nestedLines, nestedIndent, nestedWidth) continue } @@ -1395,10 +1394,7 @@ func (p *parser) renderListBlockquote(bulletWidth int) { indent := spaces(bulletWidth) // Calculate available width for blockquote content - availableWidth := p.width - bulletWidth - p.styles.blockquoteIndent - if availableWidth < 10 { - availableWidth = 10 - } + availableWidth := max(p.width-bulletWidth-p.styles.blockquoteIndent, 10) // Use renderBlockquoteContent for full support including nested code blocks fullIndent := indent + spaces(p.styles.blockquoteIndent) @@ -1430,10 +1426,9 @@ func (p *parser) renderListBlockquoteContent(lines []string, indent string, cont // Render the nested blockquote with additional indentation nestedIndent := indent + spaces(p.styles.blockquoteIndent) - nestedWidth := contentWidth - p.styles.blockquoteIndent - if nestedWidth < 10 { - nestedWidth = 10 // Minimum content width - } + nestedWidth := max(contentWidth-p.styles.blockquoteIndent, + // Minimum content width + 10) p.renderListBlockquoteContent(nestedLines, nestedIndent, nestedWidth) continue } @@ -1879,10 +1874,9 @@ func (p *parser) renderCodeBlockWithIndent(code, lang, indent string, availableW paddingRight = 0 } - contentWidth := availableWidth - paddingLeft - paddingRight - if contentWidth < 1 { - contentWidth = 1 // Minimum content width - } + contentWidth := max(availableWidth-paddingLeft-paddingRight, + // Minimum content width + 1) // Pre-compute padding strings (avoids repeated strings.Repeat calls) paddingLeftStr := spaces(paddingLeft) @@ -2053,10 +2047,7 @@ func writeSpaces(b *strings.Builder, n int) { return } for n > 0 { - chunk := n - if chunk > len(spacesBuffer) { - chunk = len(spacesBuffer) - } + chunk := min(n, len(spacesBuffer)) b.WriteString(spacesBuffer[:chunk]) n -= chunk } diff --git a/pkg/tui/components/markdown/fast_renderer_test.go b/pkg/tui/components/markdown/fast_renderer_test.go index c5cb869da..33038fbec 100644 --- a/pkg/tui/components/markdown/fast_renderer_test.go +++ b/pkg/tui/components/markdown/fast_renderer_test.go @@ -837,7 +837,7 @@ func TestFastRendererFixedWidthRectangle(t *testing.T) { out, err := r.Render(input) require.NoError(t, err) - for _, line := range strings.Split(out, "\n") { + for line := range strings.SplitSeq(out, "\n") { assert.Equal(t, 30, runewidth.StringWidth(stripANSI(line))) } } @@ -1483,7 +1483,7 @@ func TestFastRendererDeeplyNestedBlockquotes(t *testing.T) { // No literal > symbols should appear in the text content // (they should all be consumed as blockquote markers) - for _, line := range strings.Split(plain, "\n") { + for line := range strings.SplitSeq(plain, "\n") { trimmed := strings.TrimSpace(line) if trimmed != "" { assert.NotRegexp(t, `^>`, trimmed, "Line should not start with literal >: %q", trimmed) @@ -1580,10 +1580,10 @@ func TestFastRendererCodeBlockWhitespaceWrap(t *testing.T) { require.NoError(t, err) plain := stripANSI(result) - lines := strings.Split(plain, "\n") + lines := strings.SplitSeq(plain, "\n") // If wrapping occurred, it should have wrapped at a space - for _, line := range lines { + for line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" && len(trimmed) > 1 { // Line shouldn't end mid-word (unless the word itself is longer than width) @@ -1676,8 +1676,8 @@ func TestFastRendererTableSeparatorStyling(t *testing.T) { require.NoError(t, err) // The separator line should have styling applied (same as other table elements) - lines := strings.Split(result, "\n") - for _, line := range lines { + lines := strings.SplitSeq(result, "\n") + for line := range lines { plainLine := stripANSI(line) if strings.Contains(plainLine, "─") { // Separator line should have ANSI styling @@ -1700,10 +1700,7 @@ func splitIntoStreamingChunks(content string) []string { for i < len(content) { chunkSize := chunkSizes[sizeIdx%len(chunkSizes)] - end := i + chunkSize - if end > len(content) { - end = len(content) - } + end := min(i+chunkSize, len(content)) chunks = append(chunks, content[i:end]) i = end sizeIdx++ diff --git a/pkg/tui/components/message/message.go b/pkg/tui/components/message/message.go index df586e600..728f56797 100644 --- a/pkg/tui/components/message/message.go +++ b/pkg/tui/components/message/message.go @@ -110,10 +110,7 @@ func (mv *messageModel) Render(width int) string { // Create a top row with the icon pushed to the right edge // This row replaces the top padding and becomes part of the content - topPadding := innerWidth - iconWidth - if topPadding < 0 { - topPadding = 0 - } + topPadding := max(innerWidth-iconWidth, 0) topRow := strings.Repeat(" ", topPadding) + editIcon // Combine: icon row + content (icon row acts as the top padding) diff --git a/pkg/tui/components/messages/messages.go b/pkg/tui/components/messages/messages.go index 028ebbc1c..12967b5f1 100644 --- a/pkg/tui/components/messages/messages.go +++ b/pkg/tui/components/messages/messages.go @@ -2,6 +2,7 @@ package messages import ( "os" + "slices" "strconv" "strings" "sync/atomic" @@ -630,11 +631,8 @@ func (m *model) InlineEditBindings() []key.Binding { // Get the newline key help based on the configured keymap newlineKeys := m.inlineEditTextarea.KeyMap.InsertNewline.Keys() newlineHelp := "Ctrl+j" - for _, k := range newlineKeys { - if k == "shift+enter" { - newlineHelp = "Shift+Enter" - break - } + if slices.Contains(newlineKeys, "shift+enter") { + newlineHelp = "Shift+Enter" } return []key.Binding{ key.NewBinding(key.WithKeys("enter"), key.WithHelp("Enter", "save")), @@ -948,7 +946,7 @@ func (m *model) updateInlineEditTextareaHeight() { content := m.inlineEditTextarea.Value() lineCount := 0 - for _, line := range strings.Split(content, "\n") { + for line := range strings.SplitSeq(content, "\n") { lineWidth := ansi.StringWidth(line) if lineWidth == 0 { lineCount++ @@ -1553,12 +1551,12 @@ func (m *model) isEditLabelClick(msgIdx, localLine, col int) (bool, *types.Messa } plainLine := ansi.Strip(lines[localLine]) - labelIndex := strings.Index(plainLine, types.UserMessageEditLabel) - if labelIndex == -1 { + before, _, ok := strings.Cut(plainLine, types.UserMessageEditLabel) + if !ok { return false, nil } - labelStart := ansi.StringWidth(plainLine[:labelIndex]) + labelStart := ansi.StringWidth(before) labelEnd := labelStart + ansi.StringWidth(types.UserMessageEditLabel) if col >= labelStart && col < labelEnd { return true, msg @@ -1659,7 +1657,7 @@ func (m *model) StartInlineEdit(msgIndex, sessionPosition int, content string) t // Count lines and account for word wrapping lineCount := 0 if innerWidth > 0 { - for _, line := range strings.Split(content, "\n") { + for line := range strings.SplitSeq(content, "\n") { lineWidth := ansi.StringWidth(line) if lineWidth == 0 { // Empty line counts as 1 line diff --git a/pkg/tui/components/messages/messages_test.go b/pkg/tui/components/messages/messages_test.go index c83776070..b7193e07b 100644 --- a/pkg/tui/components/messages/messages_test.go +++ b/pkg/tui/components/messages/messages_test.go @@ -1,6 +1,7 @@ package messages import ( + "slices" "strconv" "strings" "testing" @@ -33,7 +34,7 @@ func TestViewDoesNotWrapWideLines(t *testing.T) { m.views = append(m.views, m.createMessageView(msg)) out := m.View() - for _, line := range strings.Split(out, "\n") { + for line := range strings.SplitSeq(out, "\n") { assert.LessOrEqual(t, ansi.StringWidth(line), 20) } } @@ -1106,11 +1107,8 @@ func TestBindingsIncludesEditKeyWhenUserMessageSelected(t *testing.T) { // Find the 'e' binding - should be present when user message is selected var foundE bool for _, b := range bindings { - for _, k := range b.Keys() { - if k == "e" { - foundE = true - break - } + if slices.Contains(b.Keys(), "e") { + foundE = true } } assert.True(t, foundE, "Bindings should include 'e' key when user message is selected") @@ -1136,11 +1134,8 @@ func TestBindingsExcludesEditKeyWhenAssistantMessageSelected(t *testing.T) { // Find the 'e' binding - should NOT be present when assistant message is selected var foundE bool for _, b := range bindings { - for _, k := range b.Keys() { - if k == "e" { - foundE = true - break - } + if slices.Contains(b.Keys(), "e") { + foundE = true } } assert.False(t, foundE, "Bindings should NOT include 'e' key when assistant message is selected") diff --git a/pkg/tui/components/sidebar/sidebar.go b/pkg/tui/components/sidebar/sidebar.go index 144a4f18d..338beb5aa 100644 --- a/pkg/tui/components/sidebar/sidebar.go +++ b/pkg/tui/components/sidebar/sidebar.go @@ -766,10 +766,7 @@ func (m *model) computeCollapsedViewModel(contentWidth int) CollapsedViewModel { // CollapsedHeight returns the number of lines needed for collapsed mode. func (m *model) CollapsedHeight(outerWidth int) int { - contentWidth := outerWidth - m.layoutCfg.PaddingLeft - m.layoutCfg.PaddingRight - if contentWidth < 1 { - contentWidth = 1 - } + contentWidth := max(outerWidth-m.layoutCfg.PaddingLeft-m.layoutCfg.PaddingRight, 1) return m.computeCollapsedViewModel(contentWidth).LineCount() } @@ -1198,10 +1195,9 @@ func (m *model) updateTitleInputWidth() { contentWidth := m.contentWidth(false) // Account for star indicator width and leave room for cursor - inputWidth := contentWidth - starWidth - 1 - if inputWidth < 10 { - inputWidth = 10 // Minimum usable width - } + inputWidth := max(contentWidth-starWidth-1, + // Minimum usable width + 10) m.titleInput.SetWidth(inputWidth) } @@ -1309,10 +1305,9 @@ func (m *model) BeginTitleEdit() { // Calculate and set the input width based on current sidebar width contentWidth := m.contentWidth(false) starWidth := lipgloss.Width(m.starIndicator()) - inputWidth := contentWidth - starWidth - 1 - if inputWidth < 10 { - inputWidth = 10 // Minimum usable width - } + inputWidth := max(contentWidth-starWidth-1, + // Minimum usable width + 10) m.titleInput.SetWidth(inputWidth) m.titleInput.Focus() diff --git a/pkg/tui/dialog/model_picker.go b/pkg/tui/dialog/model_picker.go index 60cab1c48..1b76991d9 100644 --- a/pkg/tui/dialog/model_picker.go +++ b/pkg/tui/dialog/model_picker.go @@ -291,8 +291,8 @@ func validateCustomModelSpec(spec string) error { } // Handle alloy specs (comma-separated) - parts := strings.Split(spec, ",") - for _, part := range parts { + parts := strings.SplitSeq(spec, ",") + for part := range parts { part = strings.TrimSpace(part) if part == "" { continue diff --git a/pkg/tui/dialog/multi_choice.go b/pkg/tui/dialog/multi_choice.go index 745c6cfc4..476a68ec1 100644 --- a/pkg/tui/dialog/multi_choice.go +++ b/pkg/tui/dialog/multi_choice.go @@ -260,15 +260,7 @@ func (d *multiChoiceDialog) computeDialogWidth() int { } // Calculate total dialog width - dialogWidth := maxContentWidth + frameWidth - - // Apply bounds - if dialogWidth < multiChoiceMinDialogWidth { - dialogWidth = multiChoiceMinDialogWidth - } - if dialogWidth > multiChoiceMaxDialogWidth { - dialogWidth = multiChoiceMaxDialogWidth - } + dialogWidth := min(max(maxContentWidth+frameWidth, multiChoiceMinDialogWidth), multiChoiceMaxDialogWidth) // Don't exceed screen width screenLimit := d.Width() * multiChoiceScreenWidthFactor / 100 @@ -660,10 +652,9 @@ func (d *multiChoiceDialog) renderOption(num int, label string, isSelected bool, numBoxWidth := lipgloss.Width(numBox) // Calculate available width for label (allow word wrap) - labelWidth := contentWidth - numBoxWidth - 1 // -1 for space between number box and label - if labelWidth < multiChoiceMinLabelWidth { - labelWidth = multiChoiceMinLabelWidth - } + labelWidth := max( + // -1 for space between number box and label + contentWidth-numBoxWidth-1, multiChoiceMinLabelWidth) // Apply width constraint for word wrapping var labelRendered string @@ -706,10 +697,7 @@ func (d *multiChoiceDialog) renderCustomOption(isSelected bool, contentWidth int // Calculate available width for text display // -1 for space between number box and input, -1 for cursor space - availableWidth := contentWidth - numBoxWidth - 2 - if availableWidth < multiChoiceMinLabelWidth { - availableWidth = multiChoiceMinLabelWidth - } + availableWidth := max(contentWidth-numBoxWidth-2, multiChoiceMinLabelWidth) value := d.customInput.Value() @@ -818,10 +806,7 @@ func (d *multiChoiceDialog) renderHelpAndButtons(contentWidth int) string { totalBtnWidth := secondaryWidth + multiChoiceButtonSpacing + primaryWidth // Calculate spacing between help and buttons - spacing := contentWidth - helpWidth - totalBtnWidth - if spacing < multiChoiceMinHelpSpacing { - spacing = multiChoiceMinHelpSpacing - } + spacing := max(contentWidth-helpWidth-totalBtnWidth, multiChoiceMinHelpSpacing) // Store button positions for click detection (relative to content area) d.secondaryBtnCol = helpWidth + spacing diff --git a/pkg/tui/page/chat/chat.go b/pkg/tui/page/chat/chat.go index 602e1b4e8..34b77fcae 100644 --- a/pkg/tui/page/chat/chat.go +++ b/pkg/tui/page/chat/chat.go @@ -1033,12 +1033,12 @@ func (p *chatPage) extractAttachmentsFromSession(position int) []msgtypes.Attach } // Parse "Contents of : " rest := text[len(prefix):] - colonIdx := strings.Index(rest, ": ") - if colonIdx == -1 { + before, after, ok := strings.Cut(rest, ": ") + if !ok { continue } - filename := rest[:colonIdx] - content := rest[colonIdx+2:] + filename := before + content := after if filename != "" && content != "" { attachments = append(attachments, msgtypes.Attachment{ Name: filename, diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go index 79900d699..c71553892 100644 --- a/pkg/tui/styles/styles.go +++ b/pkg/tui/styles/styles.go @@ -691,13 +691,13 @@ func MarkdownStyle() ansi.StyleConfig { BlockSuffix: "", Color: &textColor, }, - Margin: uintPtr(0), + Margin: new(uint(0)), }, BlockQuote: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ Color: &blockquoteColor, }, - Indent: uintPtr(1), + Indent: new(uint(1)), IndentToken: nil, }, List: ansi.StyleList{ @@ -707,14 +707,14 @@ func MarkdownStyle() ansi.StyleConfig { StylePrimitive: ansi.StylePrimitive{ BlockSuffix: "\n", Color: &headingColor, - Bold: boolPtr(true), + Bold: new(true), }, }, H1: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ Prefix: "## ", Color: &headingColor, - Bold: boolPtr(true), + Bold: new(true), }, }, H2: ansi.StyleBlock{ @@ -748,14 +748,14 @@ func MarkdownStyle() ansi.StyleConfig { }, }, Strikethrough: ansi.StylePrimitive{ - CrossedOut: boolPtr(true), + CrossedOut: new(true), }, Emph: ansi.StylePrimitive{ - Italic: boolPtr(true), + Italic: new(true), }, Strong: ansi.StylePrimitive{ Color: &strongColor, - Bold: boolPtr(true), + Bold: new(true), }, HorizontalRule: ansi.StylePrimitive{ Color: &hrColor, @@ -774,15 +774,15 @@ func MarkdownStyle() ansi.StyleConfig { }, Link: ansi.StylePrimitive{ Color: &linkColor, - Underline: boolPtr(true), + Underline: new(true), }, LinkText: ansi.StylePrimitive{ Color: &linkColor, - Bold: boolPtr(true), + Bold: new(true), }, Image: ansi.StylePrimitive{ Color: &linkColor, - Underline: boolPtr(true), + Underline: new(true), }, ImageText: ansi.StylePrimitive{ Color: &textSecondary, @@ -801,7 +801,7 @@ func MarkdownStyle() ansi.StyleConfig { StylePrimitive: ansi.StylePrimitive{ Color: &textSecondary, }, - Margin: uintPtr(defaultMargin), + Margin: new(uint(defaultMargin)), }, Theme: "monokai", Chroma: &ansi.Chroma{ @@ -850,8 +850,8 @@ func MarkdownStyle() ansi.StyleConfig { }, NameClass: ansi.StylePrimitive{ Color: &chromaErrorFg, - Underline: boolPtr(true), - Bold: boolPtr(true), + Underline: new(true), + Bold: new(true), }, NameDecorator: ansi.StylePrimitive{ Color: &chromaNameDecorator, @@ -872,13 +872,13 @@ func MarkdownStyle() ansi.StyleConfig { Color: &chromaGenericDeleted, }, GenericEmph: ansi.StylePrimitive{ - Italic: boolPtr(true), + Italic: new(true), }, GenericInserted: ansi.StylePrimitive{ Color: &chromaSuccess, }, GenericStrong: ansi.StylePrimitive{ - Bold: boolPtr(true), + Bold: new(true), }, GenericSubheading: ansi.StylePrimitive{ Color: &chromaGenericSubheading, @@ -903,11 +903,3 @@ func MarkdownStyle() ansi.StyleConfig { return customDarkStyle } - -func uintPtr(u uint) *uint { - return &u -} - -func boolPtr(b bool) *bool { - return &b -} diff --git a/pkg/tui/styles/theme.go b/pkg/tui/styles/theme.go index f4e082561..038d32d57 100644 --- a/pkg/tui/styles/theme.go +++ b/pkg/tui/styles/theme.go @@ -6,6 +6,7 @@ import ( "math" "os" "path/filepath" + "slices" "strconv" "strings" "sync" @@ -274,10 +275,10 @@ func listBuiltinThemeRefs() ([]string, error) { } name := entry.Name() // Accept .yaml and .yml files - if strings.HasSuffix(name, ".yaml") { - refs = append(refs, strings.TrimSuffix(name, ".yaml")) - } else if strings.HasSuffix(name, ".yml") { - refs = append(refs, strings.TrimSuffix(name, ".yml")) + if before, ok := strings.CutSuffix(name, ".yaml"); ok { + refs = append(refs, before) + } else if before, ok := strings.CutSuffix(name, ".yml"); ok { + refs = append(refs, before) } } @@ -380,10 +381,10 @@ func listThemeRefsFrom(dir string) ([]string, error) { } name := entry.Name() // Accept .yaml and .yml files - if strings.HasSuffix(name, ".yaml") { - refs = append(refs, strings.TrimSuffix(name, ".yaml")) - } else if strings.HasSuffix(name, ".yml") { - refs = append(refs, strings.TrimSuffix(name, ".yml")) + if before, ok := strings.CutSuffix(name, ".yaml"); ok { + refs = append(refs, before) + } else if before, ok := strings.CutSuffix(name, ".yml"); ok { + refs = append(refs, before) } } @@ -565,12 +566,7 @@ func IsBuiltinTheme(ref string) bool { return false } - for _, builtinRef := range builtinRefs { - if builtinRef == ref { - return true - } - } - return false + return slices.Contains(builtinRefs, ref) } // loadThemeFrom loads a theme from a specific directory (for testing). diff --git a/pkg/tui/styles/theme_test.go b/pkg/tui/styles/theme_test.go index d7865f6b5..a66b989ff 100644 --- a/pkg/tui/styles/theme_test.go +++ b/pkg/tui/styles/theme_test.go @@ -259,29 +259,20 @@ func TestDefaultTheme_AllColorsPopulated(t *testing.T) { // Check ThemeColors - all fields must be non-empty colorsVal := reflect.ValueOf(theme.Colors) - colorsType := colorsVal.Type() - for i := range colorsType.NumField() { - field := colorsType.Field(i) - value := colorsVal.Field(i).String() - assert.NotEmpty(t, value, "DefaultTheme().Colors.%s is empty - add default in DefaultTheme()", field.Name) + for field, value := range colorsVal.Fields() { + assert.NotEmpty(t, value.String(), "DefaultTheme().Colors.%s is empty - add default in DefaultTheme()", field.Name) } // Check ChromaColors - all fields must be non-empty chromaVal := reflect.ValueOf(theme.Chroma) - chromaType := chromaVal.Type() - for i := range chromaType.NumField() { - field := chromaType.Field(i) - value := chromaVal.Field(i).String() - assert.NotEmpty(t, value, "DefaultTheme().Chroma.%s is empty - add default in DefaultTheme()", field.Name) + for field, value := range chromaVal.Fields() { + assert.NotEmpty(t, value.String(), "DefaultTheme().Chroma.%s is empty - add default in DefaultTheme()", field.Name) } // Check MarkdownTheme - all fields must be non-empty mdVal := reflect.ValueOf(theme.Markdown) - mdType := mdVal.Type() - for i := range mdType.NumField() { - field := mdType.Field(i) - value := mdVal.Field(i).String() - assert.NotEmpty(t, value, "DefaultTheme().Markdown.%s is empty - add default in DefaultTheme()", field.Name) + for field, value := range mdVal.Fields() { + assert.NotEmpty(t, value.String(), "DefaultTheme().Markdown.%s is empty - add default in DefaultTheme()", field.Name) } } @@ -293,26 +284,23 @@ func TestMergeColors_HandlesAllFields(t *testing.T) { // Create a base with all fields set to "BASE" base := ThemeColors{} baseVal := reflect.ValueOf(&base).Elem() - for i := range baseVal.NumField() { - baseVal.Field(i).SetString("BASE") + for _, field := range baseVal.Fields() { + field.SetString("BASE") } // Create an override with all fields set to "OVERRIDE" override := ThemeColors{} overrideVal := reflect.ValueOf(&override).Elem() - for i := range overrideVal.NumField() { - overrideVal.Field(i).SetString("OVERRIDE") + for _, field := range overrideVal.Fields() { + field.SetString("OVERRIDE") } // Merge should replace all base values with override values merged := mergeColors(base, override) mergedVal := reflect.ValueOf(merged) - mergedType := mergedVal.Type() - for i := range mergedType.NumField() { - field := mergedType.Field(i) - value := mergedVal.Field(i).String() - assert.Equal(t, "OVERRIDE", value, + for field, value := range mergedVal.Fields() { + assert.Equal(t, "OVERRIDE", value.String(), "mergeColors() doesn't handle ThemeColors.%s - add merge logic in mergeColors()", field.Name) } } @@ -323,24 +311,21 @@ func TestMergeChromaColors_HandlesAllFields(t *testing.T) { base := ChromaColors{} baseVal := reflect.ValueOf(&base).Elem() - for i := range baseVal.NumField() { - baseVal.Field(i).SetString("BASE") + for _, field := range baseVal.Fields() { + field.SetString("BASE") } override := ChromaColors{} overrideVal := reflect.ValueOf(&override).Elem() - for i := range overrideVal.NumField() { - overrideVal.Field(i).SetString("OVERRIDE") + for _, field := range overrideVal.Fields() { + field.SetString("OVERRIDE") } merged := mergeChromaColors(base, override) mergedVal := reflect.ValueOf(merged) - mergedType := mergedVal.Type() - for i := range mergedType.NumField() { - field := mergedType.Field(i) - value := mergedVal.Field(i).String() - assert.Equal(t, "OVERRIDE", value, + for field, value := range mergedVal.Fields() { + assert.Equal(t, "OVERRIDE", value.String(), "mergeChromaColors() doesn't handle ChromaColors.%s - add merge logic", field.Name) } } @@ -351,24 +336,21 @@ func TestMergeMarkdownTheme_HandlesAllFields(t *testing.T) { base := MarkdownTheme{} baseVal := reflect.ValueOf(&base).Elem() - for i := range baseVal.NumField() { - baseVal.Field(i).SetString("BASE") + for _, field := range baseVal.Fields() { + field.SetString("BASE") } override := MarkdownTheme{} overrideVal := reflect.ValueOf(&override).Elem() - for i := range overrideVal.NumField() { - overrideVal.Field(i).SetString("OVERRIDE") + for _, field := range overrideVal.Fields() { + field.SetString("OVERRIDE") } merged := mergeMarkdownTheme(base, override) mergedVal := reflect.ValueOf(merged) - mergedType := mergedVal.Type() - for i := range mergedType.NumField() { - field := mergedType.Field(i) - value := mergedVal.Field(i).String() - assert.Equal(t, "OVERRIDE", value, + for field, value := range mergedVal.Fields() { + assert.Equal(t, "OVERRIDE", value.String(), "mergeMarkdownTheme() doesn't handle MarkdownTheme.%s - add merge logic", field.Name) } }