| 23 | const maxDetectionWorkers = 32 |
| 24 | |
| 25 | func (s *ToolService) List() ([]ToolDTO, error) { |
| 26 | registry, err := tools.LoadDefault() |
| 27 | if err != nil { |
| 28 | return nil, wrapError("TOOL_REGISTRY_LOAD_FAILED", err) |
| 29 | } |
| 30 | names := registry.Names() |
| 31 | out := make([]ToolDTO, len(names)) |
| 32 | |
| 33 | // Serve fresh cached detections synchronously; only the stale or |
| 34 | // not-yet-seen tools need a subprocess probe. This is what makes repeat |
| 35 | // Agents-page loads fast — detection is the page's dominant cost. |
| 36 | s.cache.loadOnce() |
| 37 | type pending struct { |
| 38 | idx int |
| 39 | tool tools.Tool |
| 40 | } |
| 41 | var toProbe []pending |
| 42 | for i, name := range names { |
| 43 | if entry, fresh := s.cache.get(name); fresh { |
| 44 | out[i] = toolDTOWith(registry.Tools[name], entry.Installed, entry.Version) |
| 45 | continue |
| 46 | } |
| 47 | toProbe = append(toProbe, pending{idx: i, tool: registry.Tools[name]}) |
| 48 | } |
| 49 | if len(toProbe) == 0 { |
| 50 | return out, nil |
| 51 | } |
| 52 | |
| 53 | // Each tool's detection blocks on subprocess execution (LookPath + version |
| 54 | // probes). Run them concurrently — one goroutine per tool, capped — so the |
| 55 | // Agents page doesn't wait on every binary serially. Writes target distinct |
| 56 | // slice indices, so no locking is required; output order still matches names. |
| 57 | workers := len(toProbe) |
| 58 | if workers > maxDetectionWorkers { |
| 59 | workers = maxDetectionWorkers |
| 60 | } |
| 61 | sem := make(chan struct{}, workers) |
| 62 | var wg sync.WaitGroup |
| 63 | for _, p := range toProbe { |
| 64 | wg.Add(1) |
| 65 | sem <- struct{}{} |
| 66 | go func(p pending) { |
| 67 | defer wg.Done() |
| 68 | defer func() { <-sem }() |
| 69 | installed, version := tools.Detect(p.tool) |
| 70 | s.cache.put(p.tool.Name, installed, version) |
| 71 | out[p.idx] = toolDTOWith(p.tool, installed, version) |
| 72 | }(p) |
| 73 | } |
| 74 | wg.Wait() |
| 75 | s.cache.persist() |
| 76 | return out, nil |
| 77 | } |
| 78 | |
| 79 | func (s *ToolService) Install(name string, dryRun bool) (OperationResult, error) { |
| 80 | tool, err := loadTool(name) |