fix(agent): preserve LSP diagnostics and platform paths
- commit
- 90510513a874085fe3cdc94f43c5d943c792c967
- author
- brice · · 2026-08-04 11:59 UTC
- committer
- brice · · 2026-08-04 12:56 UTC
- parent
- 7f4c198
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +60 -5
M packages/agent/lsp/client.go +1 -1
| @@ -477,7 +477,7 @@ func (c *Client) handleDiagnostics(params json.RawMessage) { | |||
| 477 | 477 | if len(value.Diagnostics) == 0 { | |
| 478 | 478 | // Preserve the URI on a clear so Manager can remove only this file's | |
| 479 | 479 | // snapshot rather than accidentally retaining stale diagnostics. | |
| 480 | - | value.Diagnostics = []Diagnostic{{Path: path, Server: c.Spec.ID}} | |
| 480 | + | value.Diagnostics = []Diagnostic{{Path: path, Server: c.Spec.ID, Clear: true}} | |
| 481 | 481 | } | |
| 482 | 482 | if c.options.OnDiagnostics != nil { | |
| 483 | 483 | c.options.OnDiagnostics(value.Diagnostics) | |
| @@ -477,7 +477,7 @@ func (c *Client) handleDiagnostics(params json.RawMessage) { | |||||
| 477 | if len(value.Diagnostics) == 0 { | 477 | if len(value.Diagnostics) == 0 { | ||
| 478 | // Preserve the URI on a clear so Manager can remove only this file's | 478 | // Preserve the URI on a clear so Manager can remove only this file's | ||
| 479 | // snapshot rather than accidentally retaining stale diagnostics. | 479 | // snapshot rather than accidentally retaining stale diagnostics. | ||
| 480 | - | value.Diagnostics = []Diagnostic{{Path: path, Server: c.Spec.ID}} | 480 | + | value.Diagnostics = []Diagnostic{{Path: path, Server: c.Spec.ID, Clear: true}} |
| 481 | } | 481 | } | ||
| 482 | if c.options.OnDiagnostics != nil { | 482 | if c.options.OnDiagnostics != nil { | ||
| 483 | c.options.OnDiagnostics(value.Diagnostics) | 483 | c.options.OnDiagnostics(value.Diagnostics) | ||
M packages/agent/lsp/config.go +13 -0
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "fmt" | |
| 8 | 8 | "os" | |
| 9 | 9 | "path/filepath" | |
| 10 | + | "runtime" | |
| 10 | 11 | "strings" | |
| 11 | 12 | ) | |
| 12 | 13 | ||
| @@ -346,6 +347,18 @@ func zotHome() string { | |||
| 346 | 347 | if value := strings.TrimSpace(os.Getenv("XDG_STATE_HOME")); value != "" { | |
| 347 | 348 | return filepath.Join(value, "zot") | |
| 348 | 349 | } | |
| 350 | + | switch runtime.GOOS { | |
| 351 | + | case "darwin": | |
| 352 | + | home, _ := os.UserHomeDir() | |
| 353 | + | if home == "" { | |
| 354 | + | return "" | |
| 355 | + | } | |
| 356 | + | return filepath.Join(home, "Library", "Application Support", "zot") | |
| 357 | + | case "windows": | |
| 358 | + | if value := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); value != "" { | |
| 359 | + | return filepath.Join(value, "zot") | |
| 360 | + | } | |
| 361 | + | } | |
| 349 | 362 | home, _ := os.UserHomeDir() | |
| 350 | 363 | if home == "" { | |
| 351 | 364 | return "" | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | "os" | 8 | "os" | ||
| 9 | "path/filepath" | 9 | "path/filepath" | ||
| 10 | + | "runtime" | |||
| 10 | "strings" | 11 | "strings" | ||
| 11 | ) | 12 | ) | ||
| 12 | 13 | ||||
| @@ -346,6 +347,18 @@ func zotHome() string { | |||||
| 346 | if value := strings.TrimSpace(os.Getenv("XDG_STATE_HOME")); value != "" { | 347 | if value := strings.TrimSpace(os.Getenv("XDG_STATE_HOME")); value != "" { | ||
| 347 | return filepath.Join(value, "zot") | 348 | return filepath.Join(value, "zot") | ||
| 348 | } | 349 | } | ||
| 350 | + | switch runtime.GOOS { | |||
| 351 | + | case "darwin": | |||
| 352 | + | home, _ := os.UserHomeDir() | |||
| 353 | + | if home == "" { | |||
| 354 | + | return "" | |||
| 355 | + | } | |||
| 356 | + | return filepath.Join(home, "Library", "Application Support", "zot") | |||
| 357 | + | case "windows": | |||
| 358 | + | if value := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); value != "" { | |||
| 359 | + | return filepath.Join(value, "zot") | |||
| 360 | + | } | |||
| 361 | + | } | |||
| 349 | home, _ := os.UserHomeDir() | 362 | home, _ := os.UserHomeDir() | ||
| 350 | if home == "" { | 363 | if home == "" { | ||
| 351 | return "" | 364 | return "" | ||
M packages/agent/lsp/lsp_test.go +35 -0
| @@ -131,6 +131,20 @@ func TestFramingRoundTripWithAdditionalHeader(t *testing.T) { | |||
| 131 | 131 | } | |
| 132 | 132 | } | |
| 133 | 133 | ||
| 134 | + | func TestZotHomeEnvironmentPrecedence(t *testing.T) { | |
| 135 | + | configuredHome := filepath.Join(t.TempDir(), "zot") | |
| 136 | + | xdgHome := filepath.Join(t.TempDir(), "state") | |
| 137 | + | t.Setenv("ZOT_HOME", configuredHome) | |
| 138 | + | t.Setenv("XDG_STATE_HOME", xdgHome) | |
| 139 | + | if got := zotHome(); got != configuredHome { | |
| 140 | + | t.Fatalf("ZOT_HOME path = %q, want %q", got, configuredHome) | |
| 141 | + | } | |
| 142 | + | t.Setenv("ZOT_HOME", "") | |
| 143 | + | if got := zotHome(); got != filepath.Join(xdgHome, "zot") { | |
| 144 | + | t.Fatalf("XDG_STATE_HOME path = %q, want %q", got, filepath.Join(xdgHome, "zot")) | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | ||
| 134 | 148 | func TestLoadConfigMergesGlobalProjectAndBuiltins(t *testing.T) { | |
| 135 | 149 | root := t.TempDir() | |
| 136 | 150 | zotHome := t.TempDir() | |
| @@ -286,6 +300,27 @@ func TestDiagnosticDedupUsesCodeAndStartPosition(t *testing.T) { | |||
| 286 | 300 | } | |
| 287 | 301 | } | |
| 288 | 302 | ||
| 303 | + | func TestManagerClearsPublishedDiagnostics(t *testing.T) { | |
| 304 | + | manager := NewManager() | |
| 305 | + | root := t.TempDir() | |
| 306 | + | path := filepath.Join(root, "main.go") | |
| 307 | + | if err := os.WriteFile(path, []byte("package main\n"), 0o644); err != nil { | |
| 308 | + | t.Fatal(err) | |
| 309 | + | } | |
| 310 | + | ws, err := manager.workspace(root) | |
| 311 | + | if err != nil { | |
| 312 | + | t.Fatal(err) | |
| 313 | + | } | |
| 314 | + | manager.storeDiagnostics(ws, "fake", []Diagnostic{{Path: path, Message: "problem"}}) | |
| 315 | + | if got := manager.snapshot(ws, path); len(got) != 1 { | |
| 316 | + | t.Fatalf("stored diagnostics = %#v", got) | |
| 317 | + | } | |
| 318 | + | manager.storeDiagnostics(ws, "fake", []Diagnostic{{Path: path, Clear: true}}) | |
| 319 | + | if got := manager.snapshot(ws, path); len(got) != 0 { | |
| 320 | + | t.Fatalf("cleared diagnostics = %#v", got) | |
| 321 | + | } | |
| 322 | + | } | |
| 323 | + | ||
| 289 | 324 | func TestDiagnosticsDedupAndGrouping(t *testing.T) { | |
| 290 | 325 | root := t.TempDir() | |
| 291 | 326 | makeDiag := func(path, message string, line int, server string) Diagnostic { | |
| @@ -131,6 +131,20 @@ func TestFramingRoundTripWithAdditionalHeader(t *testing.T) { | |||||
| 131 | } | 131 | } | ||
| 132 | } | 132 | } | ||
| 133 | 133 | ||||
| 134 | + | func TestZotHomeEnvironmentPrecedence(t *testing.T) { | |||
| 135 | + | configuredHome := filepath.Join(t.TempDir(), "zot") | |||
| 136 | + | xdgHome := filepath.Join(t.TempDir(), "state") | |||
| 137 | + | t.Setenv("ZOT_HOME", configuredHome) | |||
| 138 | + | t.Setenv("XDG_STATE_HOME", xdgHome) | |||
| 139 | + | if got := zotHome(); got != configuredHome { | |||
| 140 | + | t.Fatalf("ZOT_HOME path = %q, want %q", got, configuredHome) | |||
| 141 | + | } | |||
| 142 | + | t.Setenv("ZOT_HOME", "") | |||
| 143 | + | if got := zotHome(); got != filepath.Join(xdgHome, "zot") { | |||
| 144 | + | t.Fatalf("XDG_STATE_HOME path = %q, want %q", got, filepath.Join(xdgHome, "zot")) | |||
| 145 | + | } | |||
| 146 | + | } | |||
| 147 | + | ||||
| 134 | func TestLoadConfigMergesGlobalProjectAndBuiltins(t *testing.T) { | 148 | func TestLoadConfigMergesGlobalProjectAndBuiltins(t *testing.T) { | ||
| 135 | root := t.TempDir() | 149 | root := t.TempDir() | ||
| 136 | zotHome := t.TempDir() | 150 | zotHome := t.TempDir() | ||
| @@ -286,6 +300,27 @@ func TestDiagnosticDedupUsesCodeAndStartPosition(t *testing.T) { | |||||
| 286 | } | 300 | } | ||
| 287 | } | 301 | } | ||
| 288 | 302 | ||||
| 303 | + | func TestManagerClearsPublishedDiagnostics(t *testing.T) { | |||
| 304 | + | manager := NewManager() | |||
| 305 | + | root := t.TempDir() | |||
| 306 | + | path := filepath.Join(root, "main.go") | |||
| 307 | + | if err := os.WriteFile(path, []byte("package main\n"), 0o644); err != nil { | |||
| 308 | + | t.Fatal(err) | |||
| 309 | + | } | |||
| 310 | + | ws, err := manager.workspace(root) | |||
| 311 | + | if err != nil { | |||
| 312 | + | t.Fatal(err) | |||
| 313 | + | } | |||
| 314 | + | manager.storeDiagnostics(ws, "fake", []Diagnostic{{Path: path, Message: "problem"}}) | |||
| 315 | + | if got := manager.snapshot(ws, path); len(got) != 1 { | |||
| 316 | + | t.Fatalf("stored diagnostics = %#v", got) | |||
| 317 | + | } | |||
| 318 | + | manager.storeDiagnostics(ws, "fake", []Diagnostic{{Path: path, Clear: true}}) | |||
| 319 | + | if got := manager.snapshot(ws, path); len(got) != 0 { | |||
| 320 | + | t.Fatalf("cleared diagnostics = %#v", got) | |||
| 321 | + | } | |||
| 322 | + | } | |||
| 323 | + | ||||
| 289 | func TestDiagnosticsDedupAndGrouping(t *testing.T) { | 324 | func TestDiagnosticsDedupAndGrouping(t *testing.T) { | ||
| 290 | root := t.TempDir() | 325 | root := t.TempDir() | ||
| 291 | makeDiag := func(path, message string, line int, server string) Diagnostic { | 326 | makeDiag := func(path, message string, line int, server string) Diagnostic { | ||
M packages/agent/lsp/manager.go +8 -4
| @@ -215,21 +215,25 @@ func wsSettings(global, local map[string]any) map[string]any { | |||
| 215 | 215 | ||
| 216 | 216 | func (m *Manager) storeDiagnostics(ws *workspace, server string, diagnostics []Diagnostic) { | |
| 217 | 217 | byPath := make(map[string][]Diagnostic) | |
| 218 | + | cleared := make(map[string]bool) | |
| 218 | 219 | for _, diagnostic := range diagnostics { | |
| 219 | 220 | if diagnostic.Path == "" || !diagnosticInWorkspace(ws.rootReal, diagnostic.Path) { | |
| 220 | 221 | continue | |
| 221 | 222 | } | |
| 223 | + | if diagnostic.Clear { | |
| 224 | + | cleared[diagnostic.Path] = true | |
| 225 | + | continue | |
| 226 | + | } | |
| 222 | 227 | byPath[diagnostic.Path] = append(byPath[diagnostic.Path], diagnostic) | |
| 223 | 228 | } | |
| 224 | 229 | m.mu.Lock() | |
| 225 | 230 | if ws.diagnostics[server] == nil { | |
| 226 | 231 | ws.diagnostics[server] = make(map[string][]Diagnostic) | |
| 227 | 232 | } | |
| 233 | + | for path := range cleared { | |
| 234 | + | delete(ws.diagnostics[server], path) | |
| 235 | + | } | |
| 228 | 236 | for path, values := range byPath { | |
| 229 | - | if len(values) == 1 && values[0].Message == "" && values[0].Code == "" { | |
| 230 | - | delete(ws.diagnostics[server], path) | |
| 231 | - | continue | |
| 232 | - | } | |
| 233 | 237 | ws.diagnostics[server][path] = values | |
| 234 | 238 | } | |
| 235 | 239 | m.mu.Unlock() | |
| @@ -215,21 +215,25 @@ func wsSettings(global, local map[string]any) map[string]any { | |||||
| 215 | 215 | ||||
| 216 | func (m *Manager) storeDiagnostics(ws *workspace, server string, diagnostics []Diagnostic) { | 216 | func (m *Manager) storeDiagnostics(ws *workspace, server string, diagnostics []Diagnostic) { | ||
| 217 | byPath := make(map[string][]Diagnostic) | 217 | byPath := make(map[string][]Diagnostic) | ||
| 218 | + | cleared := make(map[string]bool) | |||
| 218 | for _, diagnostic := range diagnostics { | 219 | for _, diagnostic := range diagnostics { | ||
| 219 | if diagnostic.Path == "" || !diagnosticInWorkspace(ws.rootReal, diagnostic.Path) { | 220 | if diagnostic.Path == "" || !diagnosticInWorkspace(ws.rootReal, diagnostic.Path) { | ||
| 220 | continue | 221 | continue | ||
| 221 | } | 222 | } | ||
| 223 | + | if diagnostic.Clear { | |||
| 224 | + | cleared[diagnostic.Path] = true | |||
| 225 | + | continue | |||
| 226 | + | } | |||
| 222 | byPath[diagnostic.Path] = append(byPath[diagnostic.Path], diagnostic) | 227 | byPath[diagnostic.Path] = append(byPath[diagnostic.Path], diagnostic) | ||
| 223 | } | 228 | } | ||
| 224 | m.mu.Lock() | 229 | m.mu.Lock() | ||
| 225 | if ws.diagnostics[server] == nil { | 230 | if ws.diagnostics[server] == nil { | ||
| 226 | ws.diagnostics[server] = make(map[string][]Diagnostic) | 231 | ws.diagnostics[server] = make(map[string][]Diagnostic) | ||
| 227 | } | 232 | } | ||
| 233 | + | for path := range cleared { | |||
| 234 | + | delete(ws.diagnostics[server], path) | |||
| 235 | + | } | |||
| 228 | for path, values := range byPath { | 236 | for path, values := range byPath { | ||
| 229 | - | if len(values) == 1 && values[0].Message == "" && values[0].Code == "" { | |||
| 230 | - | delete(ws.diagnostics[server], path) | |||
| 231 | - | continue | |||
| 232 | - | } | |||
| 233 | ws.diagnostics[server][path] = values | 237 | ws.diagnostics[server][path] = values | ||
| 234 | } | 238 | } | ||
| 235 | m.mu.Unlock() | 239 | m.mu.Unlock() | ||
M packages/agent/tools/lsp.go +3 -0
| @@ -59,6 +59,9 @@ func (t *LSPTool) Description() string { | |||
| 59 | 59 | func (t *LSPTool) Schema() json.RawMessage { return json.RawMessage(lspSchema) } | |
| 60 | 60 | ||
| 61 | 61 | func (t *LSPTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) { | |
| 62 | + | if ctx == nil { | |
| 63 | + | ctx = context.Background() | |
| 64 | + | } | |
| 62 | 65 | var args lspArgs | |
| 63 | 66 | if err := json.Unmarshal(raw, &args); err != nil { | |
| 64 | 67 | return core.ToolResult{}, fmt.Errorf("invalid lsp args: %w", err) | |
| @@ -59,6 +59,9 @@ func (t *LSPTool) Description() string { | |||||
| 59 | func (t *LSPTool) Schema() json.RawMessage { return json.RawMessage(lspSchema) } | 59 | func (t *LSPTool) Schema() json.RawMessage { return json.RawMessage(lspSchema) } | ||
| 60 | 60 | ||||
| 61 | func (t *LSPTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) { | 61 | func (t *LSPTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) { | ||
| 62 | + | if ctx == nil { | |||
| 63 | + | ctx = context.Background() | |||
| 64 | + | } | |||
| 62 | var args lspArgs | 65 | var args lspArgs | ||
| 63 | if err := json.Unmarshal(raw, &args); err != nil { | 66 | if err := json.Unmarshal(raw, &args); err != nil { | ||
| 64 | return core.ToolResult{}, fmt.Errorf("invalid lsp args: %w", err) | 67 | return core.ToolResult{}, fmt.Errorf("invalid lsp args: %w", err) | ||