Skip to content

Commit e1bebf8

Browse files
authored
feat(experiment): prompt create shows more example templates and agents (#402)
1 parent dd6fef0 commit e1bebf8

File tree

6 files changed

+293
-64
lines changed

6 files changed

+293
-64
lines changed

cmd/project/create.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type promptObject struct {
4747
Title string // "Reverse string"
4848
Repository string // "slack-samples/reverse-string"
4949
Description string // "A function that reverses a given string"
50+
Subdir string // "agents/hello-world" - subdirectory within the repository
5051
}
5152

5253
const viewMoreSamples = "slack-cli#view-more-samples"
@@ -158,11 +159,15 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
158159
}
159160
}
160161

162+
subdir := createSubdirFlag
163+
if subdir == "" {
164+
subdir = template.GetSubdir()
165+
}
161166
createArgs := create.CreateArgs{
162167
AppName: appNameArg,
163168
Template: template,
164169
GitBranch: createGitBranchFlag,
165-
Subdir: createSubdirFlag,
170+
Subdir: subdir,
166171
}
167172
clients.EventTracker.SetAppTemplate(template.GetTemplatePath())
168173

cmd/project/create_template.go

Lines changed: 189 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"time"
2222

2323
"github.com/slackapi/slack-cli/internal/api"
24+
"github.com/slackapi/slack-cli/internal/experiment"
2425
"github.com/slackapi/slack-cli/internal/iostreams"
2526
"github.com/slackapi/slack-cli/internal/pkg/create"
2627
"github.com/slackapi/slack-cli/internal/shared"
@@ -30,12 +31,52 @@ import (
3031
"github.com/spf13/cobra"
3132
)
3233

34+
// getSelectionOptions returns the app template options for a given category.
3335
func getSelectionOptions(clients *shared.ClientFactory, categoryID string) []promptObject {
36+
if clients.Config.WithExperimentOn(experiment.Templates) {
37+
templatePromptObjects := map[string]([]promptObject){
38+
"slack-cli#getting-started": {
39+
{
40+
Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")),
41+
Repository: "slack-samples/bolt-js-starter-template",
42+
},
43+
{
44+
Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")),
45+
Repository: "slack-samples/bolt-python-starter-template",
46+
},
47+
},
48+
"slack-cli#ai-apps": {
49+
{
50+
Title: fmt.Sprintf("Support Agent %s", style.Secondary("Resolve IT support cases")),
51+
Repository: "slack-cli#ai-apps/support-agent",
52+
},
53+
{
54+
Title: fmt.Sprintf("Custom Agent %s", style.Secondary("Start from scratch")),
55+
Repository: "slack-cli#ai-apps/custom-agent",
56+
},
57+
},
58+
"slack-cli#automation-apps": {
59+
{
60+
Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")),
61+
Repository: "slack-samples/bolt-js-custom-function-template",
62+
},
63+
{
64+
Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")),
65+
Repository: "slack-samples/bolt-python-custom-function-template",
66+
},
67+
{
68+
Title: fmt.Sprintf("Deno Slack SDK %s", style.Secondary("Deno")),
69+
Repository: "slack-samples/deno-starter-template",
70+
},
71+
},
72+
}
73+
return templatePromptObjects[categoryID]
74+
}
75+
3476
if strings.TrimSpace(categoryID) == "" {
3577
categoryID = "slack-cli#getting-started"
3678
}
3779

38-
// App categories and templates
3980
templatePromptObjects := map[string]([]promptObject){
4081
"slack-cli#getting-started": []promptObject{
4182
{
@@ -76,6 +117,42 @@ func getSelectionOptions(clients *shared.ClientFactory, categoryID string) []pro
76117
return templatePromptObjects[categoryID]
77118
}
78119

120+
// getFrameworkOptions returns the framework choices for a given template.
121+
func getFrameworkOptions(template string) []promptObject {
122+
frameworkPromptObjects := map[string][]promptObject{
123+
"slack-cli#ai-apps/support-agent": {
124+
{
125+
Title: fmt.Sprintf("Claude Agent SDK %s", style.Secondary("Bolt for Python")),
126+
Repository: "slack-samples/bolt-python-support-agent",
127+
Subdir: "claude-agent-sdk",
128+
},
129+
{
130+
Title: fmt.Sprintf("OpenAI Agents SDK %s", style.Secondary("Bolt for Python")),
131+
Repository: "slack-samples/bolt-python-support-agent",
132+
Subdir: "openai-agents-sdk",
133+
},
134+
{
135+
Title: fmt.Sprintf("Pydantic AI %s", style.Secondary("Bolt for Python")),
136+
Repository: "slack-samples/bolt-python-support-agent",
137+
Subdir: "pydantic-ai",
138+
},
139+
},
140+
"slack-cli#ai-apps/custom-agent": {
141+
{
142+
Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")),
143+
Repository: "slack-samples/bolt-js-assistant-template",
144+
},
145+
{
146+
Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")),
147+
Repository: "slack-samples/bolt-python-assistant-template",
148+
},
149+
},
150+
}
151+
return frameworkPromptObjects[template]
152+
}
153+
154+
// getSelectionOptionsForCategory returns the top-level category options for
155+
// the create command template selection.
79156
func getSelectionOptionsForCategory(clients *shared.ClientFactory) []promptObject {
80157
return []promptObject{
81158
{
@@ -101,11 +178,16 @@ func getSelectionOptionsForCategory(clients *shared.ClientFactory) []promptObjec
101178
func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory, categoryShortcut string) (create.Template, error) {
102179
ctx := cmd.Context()
103180
var categoryID string
104-
var selectedTemplate string
105181

106182
// Check if a category shortcut was provided
107-
if categoryShortcut == "agent" {
108-
categoryID = "slack-cli#ai-apps"
183+
if categoryShortcut != "" {
184+
switch categoryShortcut {
185+
case "agent":
186+
categoryID = "slack-cli#ai-apps"
187+
default:
188+
return create.Template{}, slackerror.New(slackerror.ErrInvalidArgs).
189+
WithMessage("The %s category was not found", categoryShortcut)
190+
}
109191
} else {
110192
// Prompt for the category
111193
promptForCategory := "Select an app:"
@@ -128,73 +210,96 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory,
128210
if err != nil {
129211
return create.Template{}, slackerror.ToSlackError(err)
130212
} else if selection.Flag {
131-
selectedTemplate = selection.Option
213+
template, err := create.ResolveTemplateURL(selection.Option)
214+
if err != nil {
215+
return create.Template{}, err
216+
}
217+
confirm, err := confirmExternalTemplateSelection(cmd, clients, template)
218+
if err != nil {
219+
return create.Template{}, slackerror.ToSlackError(err)
220+
} else if !confirm {
221+
return create.Template{}, slackerror.New(slackerror.ErrUntrustedSource)
222+
}
223+
return template, nil
132224
} else if selection.Prompt {
133225
categoryID = optionsForCategory[selection.Index].Repository
134226
}
135227

136-
// Set template to view more samples, so the sample prompt is triggered
137228
if categoryID == viewMoreSamples {
138-
selectedTemplate = viewMoreSamples
229+
sampler := api.NewHTTPClient(api.HTTPClientOptions{
230+
TotalTimeOut: 60 * time.Second,
231+
})
232+
samples, err := create.GetSampleRepos(sampler)
233+
if err != nil {
234+
return create.Template{}, err
235+
}
236+
selectedSample, err := promptSampleSelection(ctx, clients, samples)
237+
if err != nil {
238+
return create.Template{}, err
239+
}
240+
return create.ResolveTemplateURL(selectedSample)
139241
}
140242
}
141243

142-
// Prompt for the template
143-
if selectedTemplate == "" {
144-
prompt := "Select a language:"
145-
options := getSelectionOptions(clients, categoryID)
146-
titles := make([]string, len(options))
147-
for i, m := range options {
148-
titles[i] = m.Title
244+
// Prompt for the example template
245+
prompt := "Select a language:"
246+
if clients.Config.WithExperimentOn(experiment.Templates) {
247+
if categoryID == "slack-cli#ai-apps" {
248+
prompt = "Select a template:"
249+
} else {
250+
prompt = "Select a language:"
149251
}
150-
template := getSelectionTemplate(clients)
151-
152-
// Print a trace with info about the template title options provided by CLI
153-
clients.IO.PrintTrace(ctx, slacktrace.CreateTemplateOptions, strings.Join(titles, ", "))
252+
}
253+
options := getSelectionOptions(clients, categoryID)
254+
titles := make([]string, len(options))
255+
for i, m := range options {
256+
titles[i] = m.Title
257+
}
258+
clients.IO.PrintTrace(ctx, slacktrace.CreateTemplateOptions, strings.Join(titles, ", "))
154259

155-
// Prompt to choose a template
156-
selection, err := clients.IO.SelectPrompt(ctx, prompt, titles, iostreams.SelectPromptConfig{
157-
Flag: clients.Config.Flags.Lookup("template"),
158-
Required: true,
159-
Template: template,
160-
})
161-
if err != nil {
162-
return create.Template{}, slackerror.ToSlackError(err)
163-
} else if selection.Flag {
164-
selectedTemplate = selection.Option
165-
} else if selection.Prompt {
166-
selectedTemplate = options[selection.Index].Repository
167-
}
260+
selection, err := clients.IO.SelectPrompt(ctx, prompt, titles, iostreams.SelectPromptConfig{
261+
Description: func(value string, index int) string {
262+
return options[index].Description
263+
},
264+
Required: true,
265+
Template: getSelectionTemplate(clients),
266+
})
267+
if err != nil {
268+
return create.Template{}, err
269+
} else if selection.Flag {
270+
return create.Template{}, slackerror.New(slackerror.ErrPrompt)
271+
} else if selection.Prompt && !strings.HasPrefix(options[selection.Index].Repository, "slack-cli#") {
272+
return create.ResolveTemplateURL(options[selection.Index].Repository)
168273
}
274+
template := options[selection.Index].Repository
169275

170-
// Ensure user is okay to proceed if template source is from a non-trusted source
171-
switch selectedTemplate {
172-
case viewMoreSamples:
173-
sampler := api.NewHTTPClient(api.HTTPClientOptions{
174-
TotalTimeOut: 60 * time.Second,
175-
})
176-
samples, err := create.GetSampleRepos(sampler)
177-
if err != nil {
178-
return create.Template{}, err
179-
}
180-
selectedSample, err := promptSampleSelection(ctx, clients, samples)
181-
if err != nil {
182-
return create.Template{}, err
183-
}
184-
return create.ResolveTemplateURL(selectedSample)
185-
default:
186-
template, err := create.ResolveTemplateURL(selectedTemplate)
187-
if err != nil {
188-
return create.Template{}, err
189-
}
190-
confirm, err := confirmExternalTemplateSelection(cmd, clients, template)
191-
if err != nil {
192-
return create.Template{}, slackerror.ToSlackError(err)
193-
} else if !confirm {
194-
return create.Template{}, slackerror.New(slackerror.ErrUntrustedSource)
195-
}
196-
return template, nil
276+
// Prompt for the example framework
277+
examples := getFrameworkOptions(template)
278+
choices := make([]string, len(examples))
279+
for i, opt := range examples {
280+
choices[i] = opt.Title
281+
}
282+
choice, err := clients.IO.SelectPrompt(ctx, "Select a language:", choices, iostreams.SelectPromptConfig{
283+
Description: func(value string, index int) string {
284+
return examples[index].Description
285+
},
286+
Required: true,
287+
Template: getSelectionTemplate(clients),
288+
})
289+
if err != nil {
290+
return create.Template{}, err
291+
} else if choice.Flag {
292+
return create.Template{}, slackerror.New(slackerror.ErrPrompt)
293+
}
294+
example := examples[choice.Index]
295+
resolved, err := create.ResolveTemplateURL(example.Repository)
296+
if err != nil {
297+
return create.Template{}, err
298+
}
299+
if example.Subdir != "" {
300+
resolved.SetSubdir(example.Subdir)
197301
}
302+
return resolved, nil
198303
}
199304

200305
// confirmExternalTemplateSelection prompts the user to confirm that they want to create an app from
@@ -243,10 +348,22 @@ func listTemplates(ctx context.Context, clients *shared.ClientFactory, categoryS
243348
}
244349

245350
var categories []categoryInfo
246-
if categoryShortcut == "agent" {
351+
if categoryShortcut == "agent" && clients.Config.WithExperimentOn(experiment.Templates) {
352+
categories = []categoryInfo{
353+
{id: "slack-cli#ai-apps/support-agent", name: "Support agent"},
354+
{id: "slack-cli#ai-apps/custom-agent", name: "Custom agent"},
355+
}
356+
} else if categoryShortcut == "agent" {
247357
categories = []categoryInfo{
248358
{id: "slack-cli#ai-apps", name: "AI Agent apps"},
249359
}
360+
} else if clients.Config.WithExperimentOn(experiment.Templates) {
361+
categories = []categoryInfo{
362+
{id: "slack-cli#getting-started", name: "Getting started"},
363+
{id: "slack-cli#ai-apps/support-agent", name: "Support agent"},
364+
{id: "slack-cli#ai-apps/custom-agent", name: "Custom agent"},
365+
{id: "slack-cli#automation-apps", name: "Automation apps"},
366+
}
250367
} else {
251368
categories = []categoryInfo{
252369
{id: "slack-cli#getting-started", name: "Getting started"},
@@ -256,10 +373,19 @@ func listTemplates(ctx context.Context, clients *shared.ClientFactory, categoryS
256373
}
257374

258375
for _, category := range categories {
259-
templates := getSelectionOptions(clients, category.id)
260-
secondary := make([]string, len(templates))
261-
for i, tmpl := range templates {
262-
secondary[i] = tmpl.Repository
376+
var secondary []string
377+
if frameworks := getFrameworkOptions(category.id); len(frameworks) > 0 {
378+
for _, tmpl := range frameworks {
379+
repo := tmpl.Repository
380+
if tmpl.Subdir != "" {
381+
repo = fmt.Sprintf("%s --subdir %s", repo, tmpl.Subdir)
382+
}
383+
secondary = append(secondary, repo)
384+
}
385+
} else {
386+
for _, tmpl := range getSelectionOptions(clients, category.id) {
387+
secondary = append(secondary, tmpl.Repository)
388+
}
263389
}
264390
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
265391
Emoji: "house_buildings",

0 commit comments

Comments
 (0)