Skip to content

Commit 8d786bd

Browse files
committed
adding suggestions for labels and fields
1 parent f80ca85 commit 8d786bd

4 files changed

Lines changed: 91 additions & 8 deletions

File tree

docs/feature-flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ runtime behavior (such as output formatting) won't appear here.
177177

178178
- **update_issue_type** - Update Issue Type
179179
- **Required OAuth Scopes**: `repo`
180+
- `is_suggestion`: If true, propose the issue type change instead of applying it. Defaults to false, which applies the change to the issue. (boolean, optional)
180181
- `issue_number`: The issue number to update (number, required)
181182
- `issue_type`: The issue type to set (string, required)
182183
- `owner`: Repository owner (username or organization) (string, required)

pkg/github/__toolsnaps__/set_issue_fields.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"description": "The GraphQL node ID of the issue field",
2424
"type": "string"
2525
},
26+
"is_suggestion": {
27+
"description": "If true, propose this field value instead of applying it. Fields not marked as suggestions are applied to the issue; suggested fields are returned as proposals in the response.",
28+
"type": "boolean"
29+
},
2630
"number_value": {
2731
"description": "The value to set for a number field",
2832
"type": "number"

pkg/github/__toolsnaps__/update_issue_labels.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
},
2323
{
2424
"properties": {
25+
"is_suggestion": {
26+
"description": "If true, propose this label instead of applying it. Labels not marked as suggestions are applied to the issue; suggested labels are returned as proposals in the response.",
27+
"type": "boolean"
28+
},
2529
"name": {
2630
"description": "Label name",
2731
"type": "string"

pkg/github/issues_granular.go

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S
320320
"State the concrete signal (e.g. 'Reports a crash when saving' → bug).",
321321
MaxLength: jsonschema.Ptr(280),
322322
},
323+
"is_suggestion": {
324+
Type: "boolean",
325+
Description: "If true, propose this label instead of applying it. " +
326+
"Labels not marked as suggestions are applied to the issue; suggested labels are returned as proposals in the response.",
327+
},
323328
},
324329
Required: []string{"name"},
325330
},
@@ -364,6 +369,7 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S
364369

365370
anyRationale := false
366371
payload := make([]any, 0, len(labelsSlice))
372+
suggested := make([]any, 0)
367373
for _, item := range labelsSlice {
368374
switch v := item.(type) {
369375
case string:
@@ -381,6 +387,14 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S
381387
if len([]rune(rationale)) > 280 {
382388
return utils.NewToolResultError("label rationale must be 280 characters or less"), nil, nil
383389
}
390+
isSuggestion, err := OptionalParam[bool](v, "is_suggestion")
391+
if err != nil {
392+
return utils.NewToolResultError(err.Error()), nil, nil
393+
}
394+
if isSuggestion {
395+
suggested = append(suggested, labelWithRationale{Name: name, Rationale: rationale})
396+
continue
397+
}
384398
if rationale == "" {
385399
payload = append(payload, name)
386400
} else {
@@ -392,6 +406,21 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S
392406
}
393407
}
394408

409+
// If no labels are to be applied, only return suggestions without
410+
// calling the API.
411+
if len(payload) == 0 {
412+
r, err := json.Marshal(map[string]any{
413+
"owner": owner,
414+
"repo": repo,
415+
"issue_number": issueNumber,
416+
"suggested_labels": suggested,
417+
})
418+
if err != nil {
419+
return utils.NewToolResultErrorFromErr("failed to marshal suggestion", err), nil, nil
420+
}
421+
return utils.NewToolResultText(string(r)), nil, nil
422+
}
423+
395424
client, err := deps.GetClient(ctx)
396425
if err != nil {
397426
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
@@ -422,10 +451,14 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S
422451
}
423452
defer func() { _ = resp.Body.Close() }()
424453

425-
r, err := json.Marshal(MinimalResponse{
426-
ID: fmt.Sprintf("%d", issue.GetID()),
427-
URL: issue.GetHTMLURL(),
428-
})
454+
respBody := map[string]any{
455+
"id": fmt.Sprintf("%d", issue.GetID()),
456+
"url": issue.GetHTMLURL(),
457+
}
458+
if len(suggested) > 0 {
459+
respBody["suggested_labels"] = suggested
460+
}
461+
r, err := json.Marshal(respBody)
429462
if err != nil {
430463
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
431464
}
@@ -961,6 +994,11 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
961994
"State the concrete signal (e.g. 'Reports a crash when saving' → high priority).",
962995
MaxLength: jsonschema.Ptr(280),
963996
},
997+
"is_suggestion": {
998+
Type: "boolean",
999+
Description: "If true, propose this field value instead of applying it. " +
1000+
"Fields not marked as suggestions are applied to the issue; suggested fields are returned as proposals in the response.",
1001+
},
9641002
},
9651003
Required: []string{"field_id"},
9661004
},
@@ -1010,6 +1048,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
10101048
}
10111049

10121050
issueFields := make([]IssueFieldCreateOrUpdateInput, 0, len(fieldMaps))
1051+
suggestedFields := make([]map[string]any, 0)
10131052
for _, fieldMap := range fieldMaps {
10141053
fieldID, err := RequiredParam[string](fieldMap, "field_id")
10151054
if err != nil {
@@ -1019,35 +1058,41 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
10191058
input := IssueFieldCreateOrUpdateInput{
10201059
FieldID: githubv4.ID(fieldID),
10211060
}
1061+
display := map[string]any{"field_id": fieldID}
10221062

10231063
// Count how many value keys are present; exactly one is required.
10241064
valueCount := 0
10251065

10261066
if v, err := OptionalParam[string](fieldMap, "text_value"); err == nil && v != "" {
10271067
input.TextValue = githubv4.NewString(githubv4.String(v))
1068+
display["text_value"] = v
10281069
valueCount++
10291070
}
10301071
if v, err := OptionalParam[float64](fieldMap, "number_value"); err == nil {
10311072
if _, exists := fieldMap["number_value"]; exists {
10321073
gqlFloat := githubv4.Float(v)
10331074
input.NumberValue = &gqlFloat
1075+
display["number_value"] = v
10341076
valueCount++
10351077
}
10361078
}
10371079
if v, err := OptionalParam[string](fieldMap, "date_value"); err == nil && v != "" {
10381080
input.DateValue = githubv4.NewString(githubv4.String(v))
1081+
display["date_value"] = v
10391082
valueCount++
10401083
}
10411084
if v, err := OptionalParam[string](fieldMap, "single_select_option_id"); err == nil && v != "" {
10421085
optionID := githubv4.ID(v)
10431086
input.SingleSelectOptionID = &optionID
1087+
display["single_select_option_id"] = v
10441088
valueCount++
10451089
}
10461090
if _, exists := fieldMap["delete"]; exists {
10471091
del, err := OptionalParam[bool](fieldMap, "delete")
10481092
if err == nil && del {
10491093
deleteVal := githubv4.Boolean(true)
10501094
input.Delete = &deleteVal
1095+
display["delete"] = true
10511096
valueCount++
10521097
}
10531098
}
@@ -1070,12 +1115,37 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
10701115
}
10711116
if rationale != "" {
10721117
input.Rationale = githubv4.NewString(githubv4.String(rationale))
1118+
display["rationale"] = rationale
10731119
}
10741120
}
10751121

1122+
isSuggestion, err := OptionalParam[bool](fieldMap, "is_suggestion")
1123+
if err != nil {
1124+
return utils.NewToolResultError(err.Error()), nil, nil
1125+
}
1126+
if isSuggestion {
1127+
suggestedFields = append(suggestedFields, display)
1128+
continue
1129+
}
1130+
10761131
issueFields = append(issueFields, input)
10771132
}
10781133

1134+
// If no fields are to be applied, only return suggestions without
1135+
// calling the API.
1136+
if len(issueFields) == 0 {
1137+
r, err := json.Marshal(map[string]any{
1138+
"owner": owner,
1139+
"repo": repo,
1140+
"issue_number": issueNumber,
1141+
"suggested_fields": suggestedFields,
1142+
})
1143+
if err != nil {
1144+
return utils.NewToolResultErrorFromErr("failed to marshal suggestion", err), nil, nil
1145+
}
1146+
return utils.NewToolResultText(string(r)), nil, nil
1147+
}
1148+
10791149
gqlClient, err := deps.GetGQLClient(ctx)
10801150
if err != nil {
10811151
return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil
@@ -1121,10 +1191,14 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
11211191
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to set issue field values", err), nil, nil
11221192
}
11231193

1124-
r, err := json.Marshal(MinimalResponse{
1125-
ID: fmt.Sprintf("%v", mutation.SetIssueFieldValue.Issue.ID),
1126-
URL: string(mutation.SetIssueFieldValue.Issue.URL),
1127-
})
1194+
respBody := map[string]any{
1195+
"id": fmt.Sprintf("%v", mutation.SetIssueFieldValue.Issue.ID),
1196+
"url": string(mutation.SetIssueFieldValue.Issue.URL),
1197+
}
1198+
if len(suggestedFields) > 0 {
1199+
respBody["suggested_fields"] = suggestedFields
1200+
}
1201+
r, err := json.Marshal(respBody)
11281202
if err != nil {
11291203
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
11301204
}

0 commit comments

Comments
 (0)