diff --git a/coderd/chatd/chattool/listtemplates.go b/coderd/chatd/chattool/listtemplates.go index cfc077da41..f11ef5d801 100644 --- a/coderd/chatd/chattool/listtemplates.go +++ b/coderd/chatd/chattool/listtemplates.go @@ -3,6 +3,7 @@ package chattool import ( "context" "database/sql" + "sort" "strings" "charm.land/fantasy" @@ -15,6 +16,8 @@ import ( "github.com/coder/coder/v2/coderd/rbac" ) +const listTemplatesPageSize = 10 + // ListTemplatesOptions configures the list_templates tool. type ListTemplatesOptions struct { DB database.Store @@ -23,16 +26,21 @@ type ListTemplatesOptions struct { type listTemplatesArgs struct { Query string `json:"query,omitempty"` + Page int `json:"page,omitempty"` } // ListTemplates returns a tool that lists available workspace templates. // The agent uses this to discover templates before creating a workspace. +// Results are ordered by number of active developers (most popular first) +// and paginated at 10 per page. func ListTemplates(options ListTemplatesOptions) fantasy.AgentTool { return fantasy.NewAgentTool( "list_templates", "List available workspace templates. Optionally filter by a "+ "search query matching template name or description. "+ - "Use this to find a template before creating a workspace.", + "Use this to find a template before creating a workspace. "+ + "Results are ordered by number of active developers (most popular first). "+ + "Returns 10 per page. Use the page parameter to paginate through results.", func(ctx context.Context, args listTemplatesArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { if options.DB == nil { return fantasy.NewTextErrorResponse("database is not configured"), nil @@ -60,8 +68,48 @@ func ListTemplates(options ListTemplatesOptions) fantasy.AgentTool { return fantasy.NewTextErrorResponse(err.Error()), nil } - items := make([]map[string]any, 0, len(templates)) - for _, t := range templates { + // Look up active developer counts so we can sort by popularity. + templateIDs := make([]uuid.UUID, len(templates)) + for i, t := range templates { + templateIDs[i] = t.ID + } + ownerCounts := make(map[uuid.UUID]int64) + if len(templateIDs) > 0 { + rows, countErr := options.DB.GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx, templateIDs) + if countErr == nil { + for _, row := range rows { + ownerCounts[row.TemplateID] = row.UniqueOwnersSum + } + } + } + + // Sort by active developer count descending. + sort.SliceStable(templates, func(i, j int) bool { + return ownerCounts[templates[i].ID] > ownerCounts[templates[j].ID] + }) + + // Paginate. + page := args.Page + if page < 1 { + page = 1 + } + totalCount := len(templates) + totalPages := (totalCount + listTemplatesPageSize - 1) / listTemplatesPageSize + if totalPages == 0 { + totalPages = 1 + } + start := (page - 1) * listTemplatesPageSize + end := start + listTemplatesPageSize + if start > totalCount { + start = totalCount + } + if end > totalCount { + end = totalCount + } + pageTemplates := templates[start:end] + + items := make([]map[string]any, 0, len(pageTemplates)) + for _, t := range pageTemplates { item := map[string]any{ "id": t.ID.String(), "name": t.Name, @@ -72,12 +120,18 @@ func ListTemplates(options ListTemplatesOptions) fantasy.AgentTool { if desc := strings.TrimSpace(t.Description); desc != "" { item["description"] = truncateRunes(desc, 200) } + if count, ok := ownerCounts[t.ID]; ok && count > 0 { + item["active_developers"] = count + } items = append(items, item) } return toolResponse(map[string]any{ - "templates": items, - "count": len(items), + "templates": items, + "count": len(items), + "page": page, + "total_pages": totalPages, + "total_count": totalCount, }), nil }, )