detached 9051051

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
477477 if len(value.Diagnostics) == 0 {
478478 // Preserve the URI on a clear so Manager can remove only this file's
479479 // 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}}
481481 }
482482 if c.options.OnDiagnostics != nil {
483483 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's478 // 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 (
77 "fmt"
88 "os"
99 "path/filepath"
10+ "runtime"
1011 "strings"
1112 )
1213
@@ -346,6 +347,18 @@ func zotHome() string {
346347 if value := strings.TrimSpace(os.Getenv("XDG_STATE_HOME")); value != "" {
347348 return filepath.Join(value, "zot")
348349 }
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+ }
349362 home, _ := os.UserHomeDir()
350363 if home == "" {
351364 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) {
131131 }
132132 }
133133
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+
134148 func TestLoadConfigMergesGlobalProjectAndBuiltins(t *testing.T) {
135149 root := t.TempDir()
136150 zotHome := t.TempDir()
@@ -286,6 +300,27 @@ func TestDiagnosticDedupUsesCodeAndStartPosition(t *testing.T) {
286300 }
287301 }
288302
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+
289324 func TestDiagnosticsDedupAndGrouping(t *testing.T) {
290325 root := t.TempDir()
291326 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 {
215215
216216 func (m *Manager) storeDiagnostics(ws *workspace, server string, diagnostics []Diagnostic) {
217217 byPath := make(map[string][]Diagnostic)
218+ cleared := make(map[string]bool)
218219 for _, diagnostic := range diagnostics {
219220 if diagnostic.Path == "" || !diagnosticInWorkspace(ws.rootReal, diagnostic.Path) {
220221 continue
221222 }
223+ if diagnostic.Clear {
224+ cleared[diagnostic.Path] = true
225+ continue
226+ }
222227 byPath[diagnostic.Path] = append(byPath[diagnostic.Path], diagnostic)
223228 }
224229 m.mu.Lock()
225230 if ws.diagnostics[server] == nil {
226231 ws.diagnostics[server] = make(map[string][]Diagnostic)
227232 }
233+ for path := range cleared {
234+ delete(ws.diagnostics[server], path)
235+ }
228236 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- }
233237 ws.diagnostics[server][path] = values
234238 }
235239 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 continue221 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] = values237 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 {
5959 func (t *LSPTool) Schema() json.RawMessage { return json.RawMessage(lspSchema) }
6060
6161 func (t *LSPTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
62+ if ctx == nil {
63+ ctx = context.Background()
64+ }
6265 var args lspArgs
6366 if err := json.Unmarshal(raw, &args); err != nil {
6467 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 lspArgs65 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)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close