on main help sign in
chore(quality): harden lint gate, throttle broker drop-log, replace flush busy-poll
309e445iko
.golangci.yml+12−4
@@ -2,6 +2,7 @@ version: "2"
2linters:
2linters:
3 enable:
3 enable:
4 - bodyclose
4 - bodyclose
5+ - errcheck
5 # - exhaustive
6 # - exhaustive
6 # - goconst
7 # - goconst
7 # - godot
8 # - godot
@@ -9,6 +10,7 @@ linters:
9 # - gomoddirectives
10 # - gomoddirectives
10 - goprintffuncname
11 - goprintffuncname
11 # - gosec
12 # - gosec
13+ - ineffassign
12 - misspell
14 - misspell
13 # - nakedret
15 # - nakedret
14 # - nestif
16 # - nestif
@@ -23,17 +25,23 @@ linters:
23 - tparallel
25 - tparallel
24 # - unconvert
26 # - unconvert
25 # - unparam
27 # - unparam
28+ - unused
26 - whitespace
29 - whitespace
27 # - wrapcheck
30 # - wrapcheck
28− disable:
29− - errcheck
30− - ineffassign
31− - unused
32 exclusions:
31 exclusions:
33 generated: lax
32 generated: lax
34 presets:
33 presets:
35 - common-false-positives
34 - 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
36 rules:
39 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
37 - text: '(slog|log)\.\w+'
45 - text: '(slog|log)\.\w+'
38 linters:
46 linters:
39 - noctx
47 - noctx
internal/agent/hyper/provider.go+1−1
@@ -88,7 +88,7 @@ func FetchCredits(ctx context.Context, apiKey string) (int, error) {
88 if err != nil {
88 if err != nil {
89 return 0, fmt.Errorf("failed to make request: %w", err)
89 return 0, fmt.Errorf("failed to make request: %w", err)
90 }
90 }
91− defer resp.Body.Close() //nolint:errcheck
91+ defer resp.Body.Close()
92
92
93 if resp.StatusCode != http.StatusOK {
93 if resp.StatusCode != http.StatusOK {
94 return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
94 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 {
60 if tr, ok := resultsByID[tc.ToolCallID]; ok {
60 if tr, ok := resultsByID[tc.ToolCallID]; ok {
61 output = toolResultOutputString(tr.Result)
61 output = toolResultOutputString(tr.Result)
62 }
62 }
63− io.WriteString(h, tc.ToolName)
63+ _, _ = io.WriteString(h, tc.ToolName)
64 h.Write([]byte{0})
64 h.Write([]byte{0})
65− io.WriteString(h, tc.Input)
65+ _, _ = io.WriteString(h, tc.Input)
66 h.Write([]byte{0})
66 h.Write([]byte{0})
67− io.WriteString(h, output)
67+ _, _ = io.WriteString(h, output)
68 h.Write([]byte{0})
68 h.Write([]byte{0})
69 }
69 }
70 return h.Sum64()
70 return h.Sum64()
internal/agent/prompt/prompt.go+1−1
@@ -115,7 +115,7 @@ func processContextPath(p string, store *config.ConfigStore) []ContextFile {
115 return contexts
115 return contexts
116 }
116 }
117 if info.IsDir() {
117 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 {
119 if err != nil {
119 if err != nil {
120 return err
120 return err
121 }
121 }
internal/agent/tools/bash.go+3−3
@@ -250,7 +250,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
250
250
251 if done {
251 if done {
252 // Command failed or completed very quickly
252 // Command failed or completed very quickly
253− bgManager.Remove(bgShell.ID)
253+ _ = bgManager.Remove(bgShell.ID)
254
254
255 interrupted := shell.IsInterrupt(execErr)
255 interrupted := shell.IsInterrupt(execErr)
256 exitCode := shell.ExitCode(execErr)
256 exitCode := shell.ExitCode(execErr)
@@ -325,7 +325,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
325 case <-ctx.Done():
325 case <-ctx.Done():
326 // Incoming context was cancelled before we moved to background
326 // Incoming context was cancelled before we moved to background
327 // Kill the shell and return error
327 // Kill the shell and return error
328− bgManager.Kill(bgShell.ID)
328+ _ = bgManager.Kill(bgShell.ID)
329 return fantasy.ToolResponse{}, ctx.Err()
329 return fantasy.ToolResponse{}, ctx.Err()
330 }
330 }
331 }
331 }
@@ -334,7 +334,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
334 // Command completed within threshold - return synchronously
334 // Command completed within threshold - return synchronously
335 // Remove from background manager since we're returning directly
335 // Remove from background manager since we're returning directly
336 // Don't call Kill() as it cancels the context and corrupts the exit code
336 // Don't call Kill() as it cancels the context and corrupts the exit code
337− bgManager.Remove(bgShell.ID)
337+ _ = bgManager.Remove(bgShell.ID)
338
338
339 interrupted := shell.IsInterrupt(execErr)
339 interrupted := shell.IsInterrupt(execErr)
340 exitCode := shell.ExitCode(execErr)
340 exitCode := shell.ExitCode(execErr)
internal/agent/tools/multiedit.go−1
@@ -445,7 +445,6 @@ func applyEditToContent(content string, edit MultiEditOperation) (string, error)
445 }
445 }
446
446
447 newContent = content[:index] + edit.NewString + content[index+len(edit.OldString):]
447 newContent = content[:index] + edit.NewString + content[index+len(edit.OldString):]
448− replacementCount = 1
449 }
448 }
450
449
451 return newContent, nil
450 return newContent, nil
internal/agent/tools/references.go−8
@@ -24,10 +24,6 @@ type ReferencesParams struct {
24 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."`
24 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."`
25}
25}
26
26
27−type referencesTool struct {
28− lspManager *lsp.Manager
29−}
30−
31const ReferencesToolName = "lsp_references"
27const ReferencesToolName = "lsp_references"
32
28
33//go:embed references.md
29//go:embed references.md
@@ -91,10 +87,6 @@ func NewReferencesTool(lspManager *lsp.Manager) fantasy.AgentTool {
91 )
87 )
92}
88}
93
89
94−func (r *referencesTool) Name() string {
95− return ReferencesToolName
96−}
97−
98func find(ctx context.Context, lspManager *lsp.Manager, symbol string, match grepMatch) ([]protocol.Location, error) {
90func find(ctx context.Context, lspManager *lsp.Manager, symbol string, match grepMatch) ([]protocol.Location, error) {
99 absPath, err := filepath.Abs(match.path)
91 absPath, err := filepath.Abs(match.path)
100 if err != nil {
92 if err != nil {
internal/app/app.go+3−1
@@ -320,7 +320,9 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
320 }
320 }
321
321
322 // force update of agent models before running so mcp tools are loaded
322 // 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+ }
324
326
325 defer stopSpinner()
327 defer stopSpinner()
326
328
internal/backend/config_test.go+7−9
@@ -81,15 +81,13 @@ func TestRemoveConfigField_PublishesConfigChanged(t *testing.T) {
81
81
82func TestUpdatePreferredModel_PublishesConfigChanged(t *testing.T) {
82func TestUpdatePreferredModel_PublishesConfigChanged(t *testing.T) {
83 if raceEnabled {
83 if raceEnabled {
84− // UpdatePreferredModel writes config.Models concurrently
84+ // The backend package has pre-existing, flaky data races under
85− // with the agent coordinator's async sub-agent builder
85+ // -race in its agent-run/attach/hold lifecycle that surface only
86− // that reads it via buildAgentModels. That race is
86+ // under full-package timing pressure. This test spins up the full
87− // pre-existing in the codebase and unrelated to this
87+ // coordinator, so it is skipped under -race to keep CI stable
88− // item; ConfigStore mutations are not currently
88+ // until that concurrency debt is addressed. The mutator → publish
89− // synchronized against background readers in [app.App].
90− // The mutator → publish wiring is unit-tested via
91− // publishConfigChanged regardless.
89+ // wiring is unit-tested via publishConfigChanged regardless.
92− t.Skip("skipped under -race: pre-existing race between ConfigStore writes and agent coordinator startup")
90+ t.Skip("skipped under -race: pre-existing backend lifecycle races")
93 }
91 }
94 b, ws, evc := newPublishingWorkspace(t)
92 b, ws, evc := newPublishingWorkspace(t)
95
93
internal/cmd/logs.go+3−3
@@ -95,7 +95,7 @@ func followLogs(ctx context.Context, logsFile string, tailLines int) error {
95 lines = lines[len(lines)-tailLines:]
95 lines = lines[len(lines)-tailLines:]
96 }
96 }
97 }
97 }
98− t.Stop()
98+ _ = t.Stop()
99
99
100 for _, line := range lines {
100 for _, line := range lines {
101 printLogLine(line)
101 printLogLine(line)
@@ -115,7 +115,7 @@ func followLogs(ctx context.Context, logsFile string, tailLines int) error {
115 if err != nil {
115 if err != nil {
116 return fmt.Errorf("failed to tail log file: %v", err)
116 return fmt.Errorf("failed to tail log file: %v", err)
117 }
117 }
118− defer t.Stop()
118+ defer func() { _ = t.Stop() }()
119
119
120 for {
120 for {
121 select {
121 select {
@@ -140,7 +140,7 @@ func showLogs(logsFile string, tailLines int) error {
140 if err != nil {
140 if err != nil {
141 return fmt.Errorf("failed to tail log file: %v", err)
141 return fmt.Errorf("failed to tail log file: %v", err)
142 }
142 }
143− defer t.Stop()
143+ defer func() { _ = t.Stop() }()
144
144
145 var lines []string
145 var lines []string
146 for line := range t.Lines {
146 for line := range t.Lines {
internal/cmd/root.go−25
@@ -3,7 +3,6 @@ package cmd
3import (
3import (
4 "bytes"
4 "bytes"
5 "context"
5 "context"
6− _ "embed"
7 "errors"
6 "errors"
8 "fmt"
7 "fmt"
9 "io"
8 "io"
@@ -840,27 +839,3 @@ func ResolveCwd(cmd *cobra.Command) (string, error) {
840 }
839 }
841 return cwd, nil
840 return cwd, nil
842}
841}
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
106 if err != nil {
106 if err != nil {
107 return result, fmt.Errorf("failed to make request: %w", err)
107 return result, fmt.Errorf("failed to make request: %w", err)
108 }
108 }
109− defer resp.Body.Close() //nolint:errcheck
109+ defer resp.Body.Close()
110
110
111 if resp.StatusCode == http.StatusNotModified {
111 if resp.StatusCode == http.StatusNotModified {
112 return result, catwalk.ErrNotModified
112 return result, catwalk.ErrNotModified
internal/config/load.go+3−1
@@ -292,7 +292,9 @@ func (c *Config) configureProviders(ctx context.Context, store *ConfigStore, env
292 // state is kept consistent by the Providers.Del call below; any
292 // state is kept consistent by the Providers.Del call below; any
293 // concurrent reload that races with this write will also see the
293 // concurrent reload that races with this write will also see the
294 // removal because it re-reads from disk.
294 // 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+ }
296 c.Providers.Del(string(p.ID))
298 c.Providers.Del(string(p.ID))
297 continue
299 continue
298 case p.ID == catwalk.InferenceProviderCopilot && config.OAuthToken != nil:
300 case p.ID == catwalk.InferenceProviderCopilot && config.OAuthToken != nil:
internal/config/store.go+3−1
@@ -906,7 +906,9 @@ func (s *ConfigStore) CaptureStalenessSnapshot(paths []string) {
906 slices.Sort(s.trackedConfigPaths)
906 slices.Sort(s.trackedConfigPaths)
907
907
908 // Capture initial snapshots
908 // Capture initial snapshots
909− s.RefreshStalenessSnapshot()
909+ if err := s.RefreshStalenessSnapshot(); err != nil {
910+ slog.Warn("failed to refresh staleness snapshot", "error", err)
911+ }
910}
912}
911
913
912// captureStalenessSnapshot is an alias for CaptureStalenessSnapshot for internal use.
914// captureStalenessSnapshot is an alias for CaptureStalenessSnapshot for internal use.
internal/herdr/client.go+6−2
@@ -158,7 +158,9 @@ func (c *Client) registerInitial() {
158 }
158 }
159 c.mu.Lock()
159 c.mu.Lock()
160 defer c.mu.Unlock()
160 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+ }
162}
164}
163
165
164// Close releases the agent's authority on the pane and shuts down
166// Close releases the agent's authority on the pane and shuts down
@@ -297,7 +299,9 @@ func (c *Client) reportLocked(state string) {
297 return
299 return
298 }
300 }
299 c.state = state
301 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+ }
301}
305}
302
306
303// reportRequest is the JSON-RPC envelope sent to herdr.
307// 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
111 })
111 })
112 if txErr != nil {
112 if txErr != nil {
113 // Rollback the transaction
113 // Rollback the transaction
114− tx.Rollback()
114+ _ = tx.Rollback()
115
115
116 // Check if this is a uniqueness constraint violation
116 // Check if this is a uniqueness constraint violation
117 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
117 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
internal/lsp/manager.go+3−1
@@ -37,7 +37,9 @@ type Manager struct {
37// NewManager creates a new LSP manager service.
37// NewManager creates a new LSP manager service.
38func NewManager(cfg *config.ConfigStore) *Manager {
38func NewManager(cfg *config.ConfigStore) *Manager {
39 manager := powernapconfig.NewManager()
39 manager := powernapconfig.NewManager()
40− manager.LoadDefaults()
40+ if err := manager.LoadDefaults(); err != nil {
41+ slog.Warn("failed to load LSP defaults", "error", err)
42+ }
41
43
42 // Merge user-configured LSPs into the manager.
44 // Merge user-configured LSPs into the manager.
43 for name, clientConfig := range cfg.Config().LSP {
45 for name, clientConfig := range cfg.Config().LSP {
internal/message/message.go+20−2
@@ -86,6 +86,11 @@ type pendingState struct {
86 // but other flushers must back off.
86 // but other flushers must back off.
87 flushing bool
87 flushing bool
88
88
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+
89 // timer is the active debounce timer, or nil if no flush is
94 // timer is the active debounce timer, or nil if no flush is
90 // scheduled. Stopped and reset when a terminal update preempts
95 // scheduled. Stopped and reset when a terminal update preempts
91 // the debounce window.
96 // the debounce window.
@@ -341,9 +346,17 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
341 s.mu.Unlock()
346 s.mu.Unlock()
342 return nil
347 return nil
343 }
348 }
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
344 s.mu.Unlock()
354 s.mu.Unlock()
345− // Brief yield; in-flight write should land in <1ms typical.
355+ select {
346− time.Sleep(time.Millisecond)
356+ case <-done:
357+ case <-ctx.Done():
358+ return ctx.Err()
359+ }
347 continue
360 continue
348 }
361 }
349 if !p.dirty {
362 if !p.dirty {
@@ -366,6 +379,7 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
366 }
379 }
367 isTerminal := shouldFlushNow(prev, &snap)
380 isTerminal := shouldFlushNow(prev, &snap)
368 p.flushing = true
381 p.flushing = true
382+ p.flushDone = make(chan struct{})
369 p.dirty = false
383 p.dirty = false
370 s.mu.Unlock()
384 s.mu.Unlock()
371
385
@@ -373,6 +387,10 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
373
387
374 s.mu.Lock()
388 s.mu.Lock()
375 p.flushing = false
389 p.flushing = false
390+ if p.flushDone != nil {
391+ close(p.flushDone)
392+ p.flushDone = nil
393+ }
376 if err == nil {
394 if err == nil {
377 p.lastFlushed = snap
395 p.lastFlushed = snap
378 p.hasFlushed = true
396 p.hasFlushed = true
internal/pubsub/broker.go+10−4
@@ -191,10 +191,16 @@ func (b *Broker[T]) Publish(t EventType, payload T) {
191 case sub <- event:
191 case sub <- event:
192 default:
192 default:
193 // Channel is full, subscriber is slow — skip this event.
193 // Channel is full, subscriber is slow — skip this event.
194− // Lossy by design; counted and logged so saturation is
194+ // Lossy by design; counted so saturation is observable via
195− // observable.
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.
196− b.dropCount.Add(1)
200+ if n := b.dropCount.Add(1); n&(n-1) == 0 {
197− slog.Warn("Pubsub buffer full; dropping event", "type", t)
201+ slog.Warn("Pubsub buffer full; dropping events",
202+ "type", t, "dropped_total", n)
203+ }
198 }
204 }
199 }
205 }
200}
206}
internal/server/proto.go+3−3
@@ -308,9 +308,9 @@ func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.R
308 // Write the SSE frame in three chunks to avoid the
308 // Write the SSE frame in three chunks to avoid the
309 // []byte->string copy that a "%s"-formatted write would
309 // []byte->string copy that a "%s"-formatted write would
310 // incur on the (potentially large) marshaled payload.
310 // incur on the (potentially large) marshaled payload.
311− w.Write([]byte("data: "))
311+ _, _ = w.Write([]byte("data: "))
312− w.Write(data)
312+ _, _ = w.Write(data)
313− w.Write([]byte("\n\n"))
313+ _, _ = w.Write([]byte("\n\n"))
314 flusher.Flush()
314 flusher.Flush()
315 }
315 }
316 }
316 }
internal/session/session.go+1−1
@@ -27,7 +27,7 @@ const (
27// HashID returns the XXH3 hash of a session ID (UUID) as a hex string.
27// HashID returns the XXH3 hash of a session ID (UUID) as a hex string.
28func HashID(id string) string {
28func HashID(id string) string {
29 h := xxh3.New()
29 h := xxh3.New()
30− h.WriteString(id)
30+ _, _ = h.WriteString(id)
31 return fmt.Sprintf("%x", h.Sum(nil))
31 return fmt.Sprintf("%x", h.Sum(nil))
32}
32}
33
33
internal/shell/background.go+1−1
@@ -185,7 +185,7 @@ func (m *BackgroundShellManager) Cleanup() int {
185 }
185 }
186
186
187 for _, id := range toRemove {
187 for _, id := range toRemove {
188− m.Remove(id)
188+ _ = m.Remove(id)
189 }
189 }
190
190
191 return len(toRemove)
191 return len(toRemove)
internal/skills/embed.go+1−1
@@ -32,7 +32,7 @@ func DiscoverBuiltinWithStates() ([]*Skill, []*SkillState) {
32 var discovered []*Skill
32 var discovered []*Skill
33 var states []*SkillState
33 var states []*SkillState
34
34
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 {
36 if err != nil {
36 if err != nil {
37 return nil
37 return nil
38 }
38 }
internal/ui/chat/docker_mcp.go+2−2
@@ -225,7 +225,7 @@ func (d *DockerMCPToolRenderContext) makeHeader(sty *styles.Styles, tool string,
225
225
226func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool string) string {
226func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool string) string {
227 mainTool := "Docker MCP"
227 mainTool := "Docker MCP"
228− action := tool
228+ var action string
229 actionStyle := sty.Tool.MCPToolName
229 actionStyle := sty.Tool.MCPToolName
230 switch tool {
230 switch tool {
231 case "mcp-exec":
231 case "mcp-exec":
@@ -254,7 +254,7 @@ func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool str
254}
254}
255
255
256func (d *DockerMCPToolRenderContext) makeCompactHeader(sty *styles.Styles, tool string, width int, params ...string) string {
256func (d *DockerMCPToolRenderContext) makeCompactHeader(sty *styles.Styles, tool string, width int, params ...string) string {
257− action := tool
257+ var action string
258 switch tool {
258 switch tool {
259 case "mcp-exec":
259 case "mcp-exec":
260 action = "exec"
260 action = "exec"
internal/ui/chat/incremental_glamour_test.go−2
@@ -737,8 +737,6 @@ func TestStreamingMarkdown_LongThinkingWithLists(t *testing.T) {
737 "final streaming output must match a fresh full render visually")
737 "final streaming output must match a fresh full render visually")
738}
738}
739
739
740−
741−
742// -----------------------------------------------------------------------
740// -----------------------------------------------------------------------
743// T5 / T6 / T7: anywhere-in-prefix hazards (B1 / B2 / B3 from the
741// T5 / T6 / T7: anywhere-in-prefix hazards (B1 / B2 / B3 from the
744// F8 round-2 review). For each hazard we drive every progressive
742// 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 {
327 // A list closes when a blank line is followed by a
327 // A list closes when a blank line is followed by a
328 // non-marker line at indent < listBaseIndent+2 (not
328 // non-marker line at indent < listBaseIndent+2 (not
329 // a continuation paragraph).
329 // a continuation paragraph).
330− listClosesHere := false
331− if listDepth > 0 && blankPending && !isMarker && indent < listBaseIndent+2 {
330+ listClosesHere := listDepth > 0 && blankPending && !isMarker && indent < listBaseIndent+2
332− listClosesHere = true
333− }
334
331
335 // Non-blank line. If a blank line preceded, this is a
332 // Non-blank line. If a blank line preceded, this is a
336 // boundary candidate at offset `off`. Evaluate BEFORE
333 // boundary candidate at offset `off`. Evaluate BEFORE
internal/ui/chat/tools.go+4−4
@@ -1367,7 +1367,7 @@ func (t *baseToolMessageItem) formatBashResultForCopy() string {
1367
1367
1368 var meta tools.BashResponseMetadata
1368 var meta tools.BashResponseMetadata
1369 if t.result.Metadata != "" {
1369 if t.result.Metadata != "" {
1370− json.Unmarshal([]byte(t.result.Metadata), &meta)
1370+ _ = json.Unmarshal([]byte(t.result.Metadata), &meta)
1371 }
1371 }
1372
1372
1373 output := meta.Output
1373 output := meta.Output
@@ -1390,7 +1390,7 @@ func (t *baseToolMessageItem) formatViewResultForCopy() string {
1390
1390
1391 var meta tools.ViewResponseMetadata
1391 var meta tools.ViewResponseMetadata
1392 if t.result.Metadata != "" {
1392 if t.result.Metadata != "" {
1393− json.Unmarshal([]byte(t.result.Metadata), &meta)
1393+ _ = json.Unmarshal([]byte(t.result.Metadata), &meta)
1394 }
1394 }
1395
1395
1396 if meta.Content == "" {
1396 if meta.Content == "" {
@@ -1461,7 +1461,7 @@ func (t *baseToolMessageItem) formatEditResultForCopy() string {
1461 }
1461 }
1462
1462
1463 var params tools.EditParams
1463 var params tools.EditParams
1464− json.Unmarshal([]byte(t.toolCall.Input), &params)
1464+ _ = json.Unmarshal([]byte(t.toolCall.Input), &params)
1465
1465
1466 var result strings.Builder
1466 var result strings.Builder
1467
1467
@@ -1496,7 +1496,7 @@ func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
1496 }
1496 }
1497
1497
1498 var params tools.MultiEditParams
1498 var params tools.MultiEditParams
1499− json.Unmarshal([]byte(t.toolCall.Input), &params)
1499+ _ = json.Unmarshal([]byte(t.toolCall.Input), &params)
1500
1500
1501 var result strings.Builder
1501 var result strings.Builder
1502 if meta.OldContent != "" || meta.NewContent != "" {
1502 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 {
315 renderedFields := lipgloss.JoinVertical(lipgloss.Left, fields...)
315 renderedFields := lipgloss.JoinVertical(lipgloss.Left, fields...)
316
316
317 // Anchor width to the longest field, capped at maxInputWidth.
317 // Anchor width to the longest field, capped at maxInputWidth.
318− const scrollbarWidth = 1
319 width := lipgloss.Width(renderedFields)
318 width := lipgloss.Width(renderedFields)
320 height := lipgloss.Height(renderedFields)
319 height := lipgloss.Height(renderedFields)
321
320
internal/ui/dialog/models_list.go−21
@@ -273,24 +273,3 @@ func (f *ModelsList) Render() string {
273 }
273 }
274 return f.List.Render()
274 return f.List.Render()
275}
275}
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 @@
1package dialog
1package dialog
2
2
3import (
3import (
4− "context"
5 "fmt"
4 "fmt"
6 "strings"
5 "strings"
7
6
@@ -66,7 +65,6 @@ type OAuth struct {
66 expiresIn int
65 expiresIn int
67 interval int
66 interval int
68 token *oauth.Token
67 token *oauth.Token
69− cancelFunc context.CancelFunc
70}
68}
71
69
72var _ Dialog = (*OAuth)(nil)
70var _ Dialog = (*OAuth)(nil)
internal/ui/dialog/permissions.go+2−4
@@ -56,10 +56,8 @@ const (
56
56
57// Permissions represents a dialog for permission requests.
57// Permissions represents a dialog for permission requests.
58type Permissions struct {
58type Permissions struct {
59− com *common.Common
59+ com *common.Common
60− windowWidth int // Terminal window dimensions.
61− windowHeight int
62− fullscreen bool // true when dialog is fullscreen
60+ fullscreen bool // true when dialog is fullscreen
63
61
64 permission permission.PermissionRequest
62 permission permission.PermissionRequest
65 selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
63 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
791
791
792 // Create a hash of the content + color to use as cache key
792 // Create a hash of the content + color to use as cache key
793 h := xxh3.New()
793 h := xxh3.New()
794− h.Write([]byte(source))
794+ _, _ = h.Write([]byte(source))
795− h.Write([]byte(colorStr))
795+ _, _ = h.Write([]byte(colorStr))
796 return fmt.Sprintf("%x", h.Sum(nil))
796 return fmt.Sprintf("%x", h.Sum(nil))
797}
797}
798
798
internal/ui/diffview/diffview_test.go−8
@@ -329,11 +329,3 @@ func assertLineWidth(t *testing.T, expected int, output string) {
329 t.Errorf("expected output width to be == %d, got %d", expected, lineWidth)
329 t.Errorf("expected output width to be == %d, got %d", expected, lineWidth)
330 }
330 }
331}
331}
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 {
1693 if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, agentCfg.Model, currentModel); err != nil {
1693 if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, agentCfg.Model, currentModel); err != nil {
1694 return util.ReportError(err)()
1694 return util.ReportError(err)()
1695 }
1695 }
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+ }
1697 status := "disabled"
1699 status := "disabled"
1698 if currentModel.Think {
1700 if currentModel.Think {
1699 status = "enabled"
1701 status = "enabled"
@@ -1768,7 +1770,9 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1768 }
1770 }
1769
1771
1770 cmds = append(cmds, func() tea.Msg {
1772 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+ }
1772 return util.NewInfoMsg("Reasoning effort set to " + msg.Effort)
1776 return util.NewInfoMsg("Reasoning effort set to " + msg.Effort)
1773 })
1777 })
1774 m.dialog.CloseDialog(dialog.ReasoningID)
1778 m.dialog.CloseDialog(dialog.ReasoningID)
@@ -3240,7 +3244,7 @@ func (m *UI) openEditor(value string) tea.Cmd {
3240 return util.ReportError(err)
3244 return util.ReportError(err)
3241 }
3245 }
3242 tmpPath := tmpfile.Name()
3246 tmpPath := tmpfile.Name()
3243− defer tmpfile.Close() //nolint:errcheck
3247+ defer tmpfile.Close()
3244 if _, err := tmpfile.WriteString(value); err != nil {
3248 if _, err := tmpfile.WriteString(value); err != nil {
3245 return util.ReportError(err)
3249 return util.ReportError(err)
3246 }
3250 }
@@ -4423,7 +4427,9 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
4423
4427
4424func (m *UI) handleStateChanged() tea.Cmd {
4428func (m *UI) handleStateChanged() tea.Cmd {
4425 return func() tea.Msg {
4429 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+ }
4427 return mcpStateChangedMsg{
4433 return mcpStateChangedMsg{
4428 states: m.com.Workspace.MCPGetStates(),
4434 states: m.com.Workspace.MCPGetStates(),
4429 }
4435 }