on main help sign in
chore(quality): harden lint gate, throttle broker drop-log, replace flush busy-poll

Tier-1 robustness + quality-gate improvements: - lint: enable errcheck, ineffassign, and unused in .golangci.yml. Adopt the std-error-handling exclusion preset (idiomatic *Close/ *Flush/Fprint*/os.Remove/os.Setenv drops) and scope errcheck off test files, so it flags only meaningful dropped errors. Fix all ~50 resulting production findings: log meaningful dropped RPC/ state errors (UpdateAgentModel, UpdateModels, RefreshStalenessSnapshot, herdr send, ...) via slog; '_ =' the idiomatic ones (hash writes, timer stops, defer rollback, best-effort cleanup); remove dead code (duplicate createDotCrushDir + gitignore assets in cmd/root.go, the unused referencesTool, and several unused dialog fields/consts/types); fix 3 ineffassign and 1 staticcheck QF1007. - pubsub: throttle the 'buffer full' warning in Broker.Publish to power-of-two drop counts so a slow subscriber can't turn saturation into a log-storm while the RLock is held. Saturation stays observable via DropCount. - message: replace the 1ms busy-poll in flushOne's sync-caller path with a per-flush done channel (closed under s.mu when flushing clears), and honor ctx cancellation while waiting. Build + golangci-lint (0 issues) + full non-race suite all green.

commit309e445eea44ef9b77b48e73d7f878734228ac82 authoriko <mail@iko.soy> dateWed 8 Jul 2026, 18:18 (3 months ago) parent8b1a0a1
34 files changed+107−132
.golangci.yml+12−4 internal/agent/hyper/provider.go+1−1 internal/agent/loop_detection.go+3−3 internal/agent/prompt/prompt.go+1−1 internal/agent/tools/bash.go+3−3 internal/agent/tools/multiedit.go−1 internal/agent/tools/references.go−8 internal/app/app.go+3−1 internal/backend/config_test.go+7−9 internal/cmd/logs.go+3−3 internal/cmd/root.go−25 internal/config/hyper.go+1−1 internal/config/load.go+3−1 internal/config/store.go+3−1 internal/herdr/client.go+6−2 internal/history/file.go+1−1 internal/lsp/manager.go+3−1 internal/message/message.go+20−2 internal/pubsub/broker.go+10−4 internal/server/proto.go+3−3 internal/session/session.go+1−1 internal/shell/background.go+1−1 internal/skills/embed.go+1−1 internal/ui/chat/docker_mcp.go+2−2 internal/ui/chat/incremental_glamour_test.go−2 internal/ui/chat/streaming_markdown.go+1−4 internal/ui/chat/tools.go+4−4 internal/ui/dialog/arguments.go−1 internal/ui/dialog/models_list.go−21 internal/ui/dialog/oauth.go−2 internal/ui/dialog/permissions.go+2−4 internal/ui/diffview/diffview.go+2−2 internal/ui/diffview/diffview_test.go−8 internal/ui/model/ui.go+10−4
.golangci.yml+12−4
@@ -2,6 +2,7 @@ version: "2"
22linters:
33 enable:
44 - bodyclose
5+ - errcheck
56 # - exhaustive
67 # - goconst
78 # - godot
@@ -9,6 +10,7 @@ linters:
910 # - gomoddirectives
1011 - goprintffuncname
1112 # - gosec
13+ - ineffassign
1214 - misspell
1315 # - nakedret
1416 # - nestif
@@ -23,17 +25,23 @@ linters:
2325 - tparallel
2426 # - unconvert
2527 # - unparam
28+ - unused
2629 - whitespace
2730 # - wrapcheck
28− disable:
29− - errcheck
30− - ineffassign
31− - unused
3231 exclusions:
3332 generated: lax
3433 presets:
3534 - common-false-positives
35+ # Ignores the idiomatic-to-drop error family (*Close, *Flush,
36+ # Fprint*, os.Remove/RemoveAll, os.Setenv, stdout/stderr writes) so
37+ # errcheck only flags genuinely meaningful dropped errors.
38+ - std-error-handling
3639 rules:
40+ # Dropped errors in tests (setup writes, httptest encoders) are
41+ # low-signal — test failures surface them anyway.
42+ - path: _test\.go
43+ linters:
44+ - errcheck
3745 - text: '(slog|log)\.\w+'
3846 linters:
3947 - noctx
internal/agent/hyper/provider.go+1−1
@@ -88,7 +88,7 @@ func FetchCredits(ctx context.Context, apiKey string) (int, error) {
8888 if err != nil {
8989 return 0, fmt.Errorf("failed to make request: %w", err)
9090 }
91− defer resp.Body.Close() //nolint:errcheck
91+ defer resp.Body.Close()
9292
9393 if resp.StatusCode != http.StatusOK {
9494 return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
internal/agent/loop_detection.go+3−3
@@ -60,11 +60,11 @@ func getToolInteractionSignature(content fantasy.ResponseContent) uint64 {
6060 if tr, ok := resultsByID[tc.ToolCallID]; ok {
6161 output = toolResultOutputString(tr.Result)
6262 }
63− io.WriteString(h, tc.ToolName)
63+ _, _ = io.WriteString(h, tc.ToolName)
6464 h.Write([]byte{0})
65− io.WriteString(h, tc.Input)
65+ _, _ = io.WriteString(h, tc.Input)
6666 h.Write([]byte{0})
67− io.WriteString(h, output)
67+ _, _ = io.WriteString(h, output)
6868 h.Write([]byte{0})
6969 }
7070 return h.Sum64()
internal/agent/prompt/prompt.go+1−1
@@ -115,7 +115,7 @@ func processContextPath(p string, store *config.ConfigStore) []ContextFile {
115115 return contexts
116116 }
117117 if info.IsDir() {
118− filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
118+ _ = filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
119119 if err != nil {
120120 return err
121121 }
internal/agent/tools/bash.go+3−3
@@ -250,7 +250,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
250250
251251 if done {
252252 // Command failed or completed very quickly
253− bgManager.Remove(bgShell.ID)
253+ _ = bgManager.Remove(bgShell.ID)
254254
255255 interrupted := shell.IsInterrupt(execErr)
256256 exitCode := shell.ExitCode(execErr)
@@ -325,7 +325,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
325325 case <-ctx.Done():
326326 // Incoming context was cancelled before we moved to background
327327 // Kill the shell and return error
328− bgManager.Kill(bgShell.ID)
328+ _ = bgManager.Kill(bgShell.ID)
329329 return fantasy.ToolResponse{}, ctx.Err()
330330 }
331331 }
@@ -334,7 +334,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
334334 // Command completed within threshold - return synchronously
335335 // Remove from background manager since we're returning directly
336336 // Don't call Kill() as it cancels the context and corrupts the exit code
337− bgManager.Remove(bgShell.ID)
337+ _ = bgManager.Remove(bgShell.ID)
338338
339339 interrupted := shell.IsInterrupt(execErr)
340340 exitCode := shell.ExitCode(execErr)
internal/agent/tools/multiedit.go−1
@@ -445,7 +445,6 @@ func applyEditToContent(content string, edit MultiEditOperation) (string, error)
445445 }
446446
447447 newContent = content[:index] + edit.NewString + content[index+len(edit.OldString):]
448− replacementCount = 1
449448 }
450449
451450 return newContent, nil
internal/agent/tools/references.go−8
@@ -24,10 +24,6 @@ type ReferencesParams struct {
2424 Path string `json:"path,omitempty" description:"The directory to search in. Use a directory/file to narrow down the symbol search. Defaults to the current working directory."`
2525}
2626
27−type referencesTool struct {
28− lspManager *lsp.Manager
29−}
30−
3127const ReferencesToolName = "lsp_references"
3228
3329//go:embed references.md
@@ -91,10 +87,6 @@ func NewReferencesTool(lspManager *lsp.Manager) fantasy.AgentTool {
9187 )
9288}
9389
94−func (r *referencesTool) Name() string {
95− return ReferencesToolName
96−}
97−
9890func find(ctx context.Context, lspManager *lsp.Manager, symbol string, match grepMatch) ([]protocol.Location, error) {
9991 absPath, err := filepath.Abs(match.path)
10092 if err != nil {
internal/app/app.go+3−1
@@ -320,7 +320,9 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
320320 }
321321
322322 // force update of agent models before running so mcp tools are loaded
323− app.AgentCoordinator.UpdateModels(ctx)
323+ if err := app.AgentCoordinator.UpdateModels(ctx); err != nil {
324+ slog.Warn("failed to update agent models", "error", err)
325+ }
324326
325327 defer stopSpinner()
326328
internal/backend/config_test.go+7−9
@@ -81,15 +81,13 @@ func TestRemoveConfigField_PublishesConfigChanged(t *testing.T) {
8181
8282func TestUpdatePreferredModel_PublishesConfigChanged(t *testing.T) {
8383 if raceEnabled {
84− // UpdatePreferredModel writes config.Models concurrently
85− // with the agent coordinator's async sub-agent builder
86− // that reads it via buildAgentModels. That race is
87− // pre-existing in the codebase and unrelated to this
88− // item; ConfigStore mutations are not currently
89− // synchronized against background readers in [app.App].
90− // The mutator → publish wiring is unit-tested via
91− // publishConfigChanged regardless.
92− t.Skip("skipped under -race: pre-existing race between ConfigStore writes and agent coordinator startup")
84+ // The backend package has pre-existing, flaky data races under
85+ // -race in its agent-run/attach/hold lifecycle that surface only
86+ // under full-package timing pressure. This test spins up the full
87+ // coordinator, so it is skipped under -race to keep CI stable
88+ // until that concurrency debt is addressed. The mutator → publish
89+ // wiring is unit-tested via publishConfigChanged regardless.
90+ t.Skip("skipped under -race: pre-existing backend lifecycle races")
9391 }
9492 b, ws, evc := newPublishingWorkspace(t)
9593
internal/cmd/logs.go+3−3
@@ -95,7 +95,7 @@ func followLogs(ctx context.Context, logsFile string, tailLines int) error {
9595 lines = lines[len(lines)-tailLines:]
9696 }
9797 }
98− t.Stop()
98+ _ = t.Stop()
9999
100100 for _, line := range lines {
101101 printLogLine(line)
@@ -115,7 +115,7 @@ func followLogs(ctx context.Context, logsFile string, tailLines int) error {
115115 if err != nil {
116116 return fmt.Errorf("failed to tail log file: %v", err)
117117 }
118− defer t.Stop()
118+ defer func() { _ = t.Stop() }()
119119
120120 for {
121121 select {
@@ -140,7 +140,7 @@ func showLogs(logsFile string, tailLines int) error {
140140 if err != nil {
141141 return fmt.Errorf("failed to tail log file: %v", err)
142142 }
143− defer t.Stop()
143+ defer func() { _ = t.Stop() }()
144144
145145 var lines []string
146146 for line := range t.Lines {
internal/cmd/root.go−25
@@ -3,7 +3,6 @@ package cmd
33import (
44 "bytes"
55 "context"
6− _ "embed"
76 "errors"
87 "fmt"
98 "io"
@@ -840,27 +839,3 @@ func ResolveCwd(cmd *cobra.Command) (string, error) {
840839 }
841840 return cwd, nil
842841}
843−
844−func createDotCrushDir(dir string) error {
845− if err := os.MkdirAll(dir, 0o700); err != nil {
846− return fmt.Errorf("failed to create data directory: %q %w", dir, err)
847− }
848−
849− gitIgnorePath := filepath.Join(dir, ".gitignore")
850− content, err := os.ReadFile(gitIgnorePath)
851−
852− // create or update if old version
853− if os.IsNotExist(err) || string(content) == oldGitIgnore {
854− if err := os.WriteFile(gitIgnorePath, []byte(defaultGitIgnore), 0o644); err != nil {
855− return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
856− }
857− }
858−
859− return nil
860−}
861−
862−//go:embed gitignore/old
863−var oldGitIgnore string
864−
865−//go:embed gitignore/default
866−var defaultGitIgnore string
internal/config/hyper.go+1−1
@@ -106,7 +106,7 @@ func (r realHyperClient) Get(ctx context.Context, etag string) (catwalk.Provider
106106 if err != nil {
107107 return result, fmt.Errorf("failed to make request: %w", err)
108108 }
109− defer resp.Body.Close() //nolint:errcheck
109+ defer resp.Body.Close()
110110
111111 if resp.StatusCode == http.StatusNotModified {
112112 return result, catwalk.ErrNotModified
internal/config/load.go+3−1
@@ -292,7 +292,9 @@ func (c *Config) configureProviders(ctx context.Context, store *ConfigStore, env
292292 // state is kept consistent by the Providers.Del call below; any
293293 // concurrent reload that races with this write will also see the
294294 // removal because it re-reads from disk.
295− store.RemoveConfigField(ScopeGlobal, "providers.anthropic")
295+ if err := store.RemoveConfigField(ScopeGlobal, "providers.anthropic"); err != nil {
296+ slog.Warn("failed to remove anthropic provider config field", "error", err)
297+ }
296298 c.Providers.Del(string(p.ID))
297299 continue
298300 case p.ID == catwalk.InferenceProviderCopilot && config.OAuthToken != nil:
internal/config/store.go+3−1
@@ -906,7 +906,9 @@ func (s *ConfigStore) CaptureStalenessSnapshot(paths []string) {
906906 slices.Sort(s.trackedConfigPaths)
907907
908908 // Capture initial snapshots
909− s.RefreshStalenessSnapshot()
909+ if err := s.RefreshStalenessSnapshot(); err != nil {
910+ slog.Warn("failed to refresh staleness snapshot", "error", err)
911+ }
910912}
911913
912914// captureStalenessSnapshot is an alias for CaptureStalenessSnapshot for internal use.
internal/herdr/client.go+6−2
@@ -158,7 +158,9 @@ func (c *Client) registerInitial() {
158158 }
159159 c.mu.Lock()
160160 defer c.mu.Unlock()
161− c.snd.send(c.newRequestLocked("pane.report_agent", "init", stateIdle))
161+ if err := c.snd.send(c.newRequestLocked("pane.report_agent", "init", stateIdle)); err != nil {
162+ slog.Warn("failed to send herdr agent report", "error", err)
163+ }
162164}
163165
164166// Close releases the agent's authority on the pane and shuts down
@@ -297,7 +299,9 @@ func (c *Client) reportLocked(state string) {
297299 return
298300 }
299301 c.state = state
300− c.snd.send(c.newRequestLocked("pane.report_agent", "report", state))
302+ if err := c.snd.send(c.newRequestLocked("pane.report_agent", "report", state)); err != nil {
303+ slog.Warn("failed to send herdr agent report", "error", err)
304+ }
301305}
302306
303307// reportRequest is the JSON-RPC envelope sent to herdr.
internal/history/file.go+1−1
@@ -111,7 +111,7 @@ func (s *service) createWithVersion(ctx context.Context, sessionID, path, conten
111111 })
112112 if txErr != nil {
113113 // Rollback the transaction
114− tx.Rollback()
114+ _ = tx.Rollback()
115115
116116 // Check if this is a uniqueness constraint violation
117117 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
internal/lsp/manager.go+3−1
@@ -37,7 +37,9 @@ type Manager struct {
3737// NewManager creates a new LSP manager service.
3838func NewManager(cfg *config.ConfigStore) *Manager {
3939 manager := powernapconfig.NewManager()
40− manager.LoadDefaults()
40+ if err := manager.LoadDefaults(); err != nil {
41+ slog.Warn("failed to load LSP defaults", "error", err)
42+ }
4143
4244 // Merge user-configured LSPs into the manager.
4345 for name, clientConfig := range cfg.Config().LSP {
internal/message/message.go+20−2
@@ -86,6 +86,11 @@ type pendingState struct {
8686 // but other flushers must back off.
8787 flushing bool
8888
89+ // flushDone is non-nil exactly while flushing is true. It is closed
90+ // (under service.mu) when the in-flight flush completes, so a sync
91+ // caller can wait on it instead of polling.
92+ flushDone chan struct{}
93+
8994 // timer is the active debounce timer, or nil if no flush is
9095 // scheduled. Stopped and reset when a terminal update preempts
9196 // the debounce window.
@@ -341,9 +346,17 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
341346 s.mu.Unlock()
342347 return nil
343348 }
349+ // Wait for the in-flight flush to finish rather than
350+ // polling. flushDone is closed under s.mu when flushing
351+ // clears, and we capture it under the same lock, so the
352+ // wakeup can never be missed.
353+ done := p.flushDone
344354 s.mu.Unlock()
345− // Brief yield; in-flight write should land in <1ms typical.
346− time.Sleep(time.Millisecond)
355+ select {
356+ case <-done:
357+ case <-ctx.Done():
358+ return ctx.Err()
359+ }
347360 continue
348361 }
349362 if !p.dirty {
@@ -366,6 +379,7 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
366379 }
367380 isTerminal := shouldFlushNow(prev, &snap)
368381 p.flushing = true
382+ p.flushDone = make(chan struct{})
369383 p.dirty = false
370384 s.mu.Unlock()
371385
@@ -373,6 +387,10 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
373387
374388 s.mu.Lock()
375389 p.flushing = false
390+ if p.flushDone != nil {
391+ close(p.flushDone)
392+ p.flushDone = nil
393+ }
376394 if err == nil {
377395 p.lastFlushed = snap
378396 p.hasFlushed = true
internal/pubsub/broker.go+10−4
@@ -191,10 +191,16 @@ func (b *Broker[T]) Publish(t EventType, payload T) {
191191 case sub <- event:
192192 default:
193193 // Channel is full, subscriber is slow — skip this event.
194− // Lossy by design; counted and logged so saturation is
195− // observable.
196− b.dropCount.Add(1)
197− slog.Warn("Pubsub buffer full; dropping event", "type", t)
194+ // Lossy by design; counted so saturation is observable via
195+ // DropCount. The warning is throttled to power-of-two drop
196+ // counts (1, 2, 4, 8, …): saturation is exactly when this
197+ // path runs hottest, and an unthrottled per-event log — while
198+ // holding the RLock — would turn a slow subscriber into a
199+ // log-storm that slows every publisher further.
200+ if n := b.dropCount.Add(1); n&(n-1) == 0 {
201+ slog.Warn("Pubsub buffer full; dropping events",
202+ "type", t, "dropped_total", n)
203+ }
198204 }
199205 }
200206}
internal/server/proto.go+3−3
@@ -308,9 +308,9 @@ func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.R
308308 // Write the SSE frame in three chunks to avoid the
309309 // []byte->string copy that a "%s"-formatted write would
310310 // incur on the (potentially large) marshaled payload.
311− w.Write([]byte("data: "))
312− w.Write(data)
313− w.Write([]byte("\n\n"))
311+ _, _ = w.Write([]byte("data: "))
312+ _, _ = w.Write(data)
313+ _, _ = w.Write([]byte("\n\n"))
314314 flusher.Flush()
315315 }
316316 }
internal/session/session.go+1−1
@@ -27,7 +27,7 @@ const (
2727// HashID returns the XXH3 hash of a session ID (UUID) as a hex string.
2828func HashID(id string) string {
2929 h := xxh3.New()
30− h.WriteString(id)
30+ _, _ = h.WriteString(id)
3131 return fmt.Sprintf("%x", h.Sum(nil))
3232}
3333
internal/shell/background.go+1−1
@@ -185,7 +185,7 @@ func (m *BackgroundShellManager) Cleanup() int {
185185 }
186186
187187 for _, id := range toRemove {
188− m.Remove(id)
188+ _ = m.Remove(id)
189189 }
190190
191191 return len(toRemove)
internal/skills/embed.go+1−1
@@ -32,7 +32,7 @@ func DiscoverBuiltinWithStates() ([]*Skill, []*SkillState) {
3232 var discovered []*Skill
3333 var states []*SkillState
3434
35− fs.WalkDir(builtinFS, "builtin", func(path string, d fs.DirEntry, err error) error {
35+ _ = fs.WalkDir(builtinFS, "builtin", func(path string, d fs.DirEntry, err error) error {
3636 if err != nil {
3737 return nil
3838 }
internal/ui/chat/docker_mcp.go+2−2
@@ -225,7 +225,7 @@ func (d *DockerMCPToolRenderContext) makeHeader(sty *styles.Styles, tool string,
225225
226226func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool string) string {
227227 mainTool := "Docker MCP"
228− action := tool
228+ var action string
229229 actionStyle := sty.Tool.MCPToolName
230230 switch tool {
231231 case "mcp-exec":
@@ -254,7 +254,7 @@ func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool str
254254}
255255
256256func (d *DockerMCPToolRenderContext) makeCompactHeader(sty *styles.Styles, tool string, width int, params ...string) string {
257− action := tool
257+ var action string
258258 switch tool {
259259 case "mcp-exec":
260260 action = "exec"
internal/ui/chat/incremental_glamour_test.go−2
@@ -737,8 +737,6 @@ func TestStreamingMarkdown_LongThinkingWithLists(t *testing.T) {
737737 "final streaming output must match a fresh full render visually")
738738}
739739
740−
741−
742740// -----------------------------------------------------------------------
743741// T5 / T6 / T7: anywhere-in-prefix hazards (B1 / B2 / B3 from the
744742// F8 round-2 review). For each hazard we drive every progressive
internal/ui/chat/streaming_markdown.go+1−4
@@ -327,10 +327,7 @@ func scanSafeBoundaries(content string) []int {
327327 // A list closes when a blank line is followed by a
328328 // non-marker line at indent < listBaseIndent+2 (not
329329 // a continuation paragraph).
330− listClosesHere := false
331− if listDepth > 0 && blankPending && !isMarker && indent < listBaseIndent+2 {
332− listClosesHere = true
333− }
330+ listClosesHere := listDepth > 0 && blankPending && !isMarker && indent < listBaseIndent+2
334331
335332 // Non-blank line. If a blank line preceded, this is a
336333 // boundary candidate at offset `off`. Evaluate BEFORE
internal/ui/chat/tools.go+4−4
@@ -1367,7 +1367,7 @@ func (t *baseToolMessageItem) formatBashResultForCopy() string {
13671367
13681368 var meta tools.BashResponseMetadata
13691369 if t.result.Metadata != "" {
1370− json.Unmarshal([]byte(t.result.Metadata), &meta)
1370+ _ = json.Unmarshal([]byte(t.result.Metadata), &meta)
13711371 }
13721372
13731373 output := meta.Output
@@ -1390,7 +1390,7 @@ func (t *baseToolMessageItem) formatViewResultForCopy() string {
13901390
13911391 var meta tools.ViewResponseMetadata
13921392 if t.result.Metadata != "" {
1393− json.Unmarshal([]byte(t.result.Metadata), &meta)
1393+ _ = json.Unmarshal([]byte(t.result.Metadata), &meta)
13941394 }
13951395
13961396 if meta.Content == "" {
@@ -1461,7 +1461,7 @@ func (t *baseToolMessageItem) formatEditResultForCopy() string {
14611461 }
14621462
14631463 var params tools.EditParams
1464− json.Unmarshal([]byte(t.toolCall.Input), &params)
1464+ _ = json.Unmarshal([]byte(t.toolCall.Input), &params)
14651465
14661466 var result strings.Builder
14671467
@@ -1496,7 +1496,7 @@ func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
14961496 }
14971497
14981498 var params tools.MultiEditParams
1499− json.Unmarshal([]byte(t.toolCall.Input), &params)
1499+ _ = json.Unmarshal([]byte(t.toolCall.Input), &params)
15001500
15011501 var result strings.Builder
15021502 if meta.OldContent != "" || meta.NewContent != "" {
internal/ui/dialog/arguments.go−1
@@ -315,7 +315,6 @@ func (a *Arguments) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
315315 renderedFields := lipgloss.JoinVertical(lipgloss.Left, fields...)
316316
317317 // Anchor width to the longest field, capped at maxInputWidth.
318− const scrollbarWidth = 1
319318 width := lipgloss.Width(renderedFields)
320319 height := lipgloss.Height(renderedFields)
321320
internal/ui/dialog/models_list.go−21
@@ -273,24 +273,3 @@ func (f *ModelsList) Render() string {
273273 }
274274 return f.List.Render()
275275}
276−
277−type modelGroups []ModelGroup
278−
279−func (m modelGroups) Len() int {
280− n := 0
281− for _, g := range m {
282− n += len(g.Items)
283− }
284− return n
285−}
286−
287−func (m modelGroups) String(i int) string {
288− count := 0
289− for _, g := range m {
290− if i < count+len(g.Items) {
291− return g.Items[i-count].Filter()
292− }
293− count += len(g.Items)
294− }
295− return ""
296−}
internal/ui/dialog/oauth.go−2
@@ -1,7 +1,6 @@
11package dialog
22
33import (
4− "context"
54 "fmt"
65 "strings"
76
@@ -66,7 +65,6 @@ type OAuth struct {
6665 expiresIn int
6766 interval int
6867 token *oauth.Token
69− cancelFunc context.CancelFunc
7068}
7169
7270var _ Dialog = (*OAuth)(nil)
internal/ui/dialog/permissions.go+2−4
@@ -56,10 +56,8 @@ const (
5656
5757// Permissions represents a dialog for permission requests.
5858type Permissions struct {
59− com *common.Common
60− windowWidth int // Terminal window dimensions.
61− windowHeight int
62− fullscreen bool // true when dialog is fullscreen
59+ com *common.Common
60+ fullscreen bool // true when dialog is fullscreen
6361
6462 permission permission.PermissionRequest
6563 selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
internal/ui/diffview/diffview.go+2−2
@@ -791,8 +791,8 @@ func (dv *DiffView) createSyntaxCacheKey(source string, bgColor color.Color) str
791791
792792 // Create a hash of the content + color to use as cache key
793793 h := xxh3.New()
794− h.Write([]byte(source))
795− h.Write([]byte(colorStr))
794+ _, _ = h.Write([]byte(source))
795+ _, _ = h.Write([]byte(colorStr))
796796 return fmt.Sprintf("%x", h.Sum(nil))
797797}
798798
internal/ui/diffview/diffview_test.go−8
@@ -329,11 +329,3 @@ func assertLineWidth(t *testing.T, expected int, output string) {
329329 t.Errorf("expected output width to be == %d, got %d", expected, lineWidth)
330330 }
331331}
332−
333−func assertHeight(t *testing.T, expected int, output string) {
334− output = strings.TrimSuffix(output, "\n")
335− lines := strings.Count(output, "\n") + 1
336− if lines != expected {
337− t.Errorf("expected output height to be == %d, got %d", expected, lines)
338− }
339−}
internal/ui/model/ui.go+10−4
@@ -1693,7 +1693,9 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
16931693 if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, agentCfg.Model, currentModel); err != nil {
16941694 return util.ReportError(err)()
16951695 }
1696− m.com.Workspace.UpdateAgentModel(context.TODO())
1696+ if err := m.com.Workspace.UpdateAgentModel(context.TODO()); err != nil {
1697+ slog.Warn("failed to update agent model", "error", err)
1698+ }
16971699 status := "disabled"
16981700 if currentModel.Think {
16991701 status = "enabled"
@@ -1768,7 +1770,9 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
17681770 }
17691771
17701772 cmds = append(cmds, func() tea.Msg {
1771− m.com.Workspace.UpdateAgentModel(context.TODO())
1773+ if err := m.com.Workspace.UpdateAgentModel(context.TODO()); err != nil {
1774+ slog.Warn("failed to update agent model", "error", err)
1775+ }
17721776 return util.NewInfoMsg("Reasoning effort set to " + msg.Effort)
17731777 })
17741778 m.dialog.CloseDialog(dialog.ReasoningID)
@@ -3240,7 +3244,7 @@ func (m *UI) openEditor(value string) tea.Cmd {
32403244 return util.ReportError(err)
32413245 }
32423246 tmpPath := tmpfile.Name()
3243− defer tmpfile.Close() //nolint:errcheck
3247+ defer tmpfile.Close()
32443248 if _, err := tmpfile.WriteString(value); err != nil {
32453249 return util.ReportError(err)
32463250 }
@@ -4423,7 +4427,9 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
44234427
44244428func (m *UI) handleStateChanged() tea.Cmd {
44254429 return func() tea.Msg {
4426− m.com.Workspace.UpdateAgentModel(context.Background())
4430+ if err := m.com.Workspace.UpdateAgentModel(context.Background()); err != nil {
4431+ slog.Warn("failed to update agent model", "error", err)
4432+ }
44274433 return mcpStateChangedMsg{
44284434 states: m.com.Workspace.MCPGetStates(),
44294435 }