Skip to content

Commit 236ac9f

Browse files
[AXON-385] Use JIRA AI to populate createIssueView fileds
1 parent bba8962 commit 236ac9f

File tree

20 files changed

+823
-82
lines changed

20 files changed

+823
-82
lines changed

e2e/tsconfig.e2e.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"outDir": "../.generated",
5+
"module": "CommonJS",
6+
"target": "es2018",
7+
"lib": ["ESNext", "dom"],
8+
"alwaysStrict": true,
9+
"resolveJsonModule": true,
10+
"skipLibCheck": true,
11+
"sourceMap": true,
12+
"allowJs": true,
13+
"jsx": "react",
14+
"moduleResolution": "node",
15+
"rootDir": "../..",
16+
"allowSyntheticDefaultImports": true,
17+
"forceConsistentCasingInFileNames": true,
18+
"noImplicitReturns": true,
19+
"noImplicitThis": true,
20+
"noImplicitAny": true,
21+
"strictNullChecks": true,
22+
"noUnusedLocals": true,
23+
"noFallthroughCasesInSwitch": true,
24+
"strictPropertyInitialization": true,
25+
"typeRoots": ["node_modules/@types", "src/typings/"],
26+
"paths": {
27+
"testsutil/*": ["testsutil/*"]
28+
}
29+
},
30+
"include": ["tests/**/*"]
31+
}

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,21 @@
809809
"type": "object",
810810
"title": "Atlassian",
811811
"properties": {
812+
"atlascode.issueSuggestion.enabled": {
813+
"type": "boolean",
814+
"default": true,
815+
"description": "Enables AI-assisted issue suggestion for TODO comments",
816+
"scope": "window"
817+
},
818+
"atlascode.issueSuggestion.contextLevel": {
819+
"type": "string",
820+
"description": "What context to use for issue suggestions - only the TODO text, surrouding code, or the entire file",
821+
"default": "codeContext",
822+
"enum": [
823+
"todoOnly",
824+
"codeContext"
825+
]
826+
},
812827
"atlascode.outputLevel": {
813828
"type": "string",
814829
"default": "silent",

src/atlclients/issueBuilder.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { AxiosInstance } from 'axios';
2+
3+
import { Container } from '../container';
4+
import { getAxiosInstance } from '../jira/jira-client/providers';
5+
import { BasicAuthInfo, DetailedSiteInfo, isBasicAuthInfo, ProductJira } from './authInfo';
6+
7+
export type SuggestedIssue = {
8+
issueType: string;
9+
fieldValues: {
10+
summary: string;
11+
description: string;
12+
};
13+
};
14+
15+
export type SuggestedIssuesResponse = {
16+
suggestedIssues: SuggestedIssue[];
17+
};
18+
19+
type SiteId = string;
20+
21+
const ASSIST_API = '/api/assist/api/ai/v2/ai-feature/jira/issue/source-type/conversation/suggestions';
22+
23+
export const isSiteCloudWithApiKey = async (site?: DetailedSiteInfo | SiteId): Promise<boolean> => {
24+
const siteToCheck = typeof site === 'string' ? Container.siteManager.getSiteForId(ProductJira, site) : site;
25+
26+
if (!siteToCheck || !siteToCheck.host) {
27+
return false;
28+
}
29+
30+
if (!siteToCheck.host.endsWith('.atlassian.net')) {
31+
return false;
32+
}
33+
34+
const authInfo = await Container.credentialManager.getAuthInfo(siteToCheck);
35+
if (!authInfo || !isBasicAuthInfo(authInfo)) {
36+
return false;
37+
}
38+
39+
return true;
40+
};
41+
42+
export const findCloudSiteWithApiKey = async (): Promise<DetailedSiteInfo | undefined> => {
43+
const sites = (
44+
await Promise.all(
45+
Container.siteManager
46+
.getSitesAvailable(ProductJira)
47+
.map(async (x) => ((await isSiteCloudWithApiKey(x)) ? x : undefined)),
48+
)
49+
).filter((x) => x !== undefined) as DetailedSiteInfo[];
50+
51+
// Any site is fine, just need an API key
52+
return sites.length > 0 ? sites[0] : undefined;
53+
};
54+
55+
export const fetchIssueSuggestions = async (prompt: string): Promise<SuggestedIssuesResponse> => {
56+
const axiosInstance: AxiosInstance = getAxiosInstance();
57+
58+
try {
59+
const site = await findCloudSiteWithApiKey();
60+
61+
if (!site) {
62+
throw new Error('No site found with API key');
63+
}
64+
65+
const authInfo = (await Container.credentialManager.getAuthInfo(site)) as BasicAuthInfo;
66+
if (!authInfo || !isBasicAuthInfo(authInfo)) {
67+
throw new Error('No valid auth info found for site');
68+
}
69+
70+
const response = await axiosInstance.post(
71+
`https://${site.host}/gateway` + ASSIST_API,
72+
buildRequestBody(prompt),
73+
{
74+
headers: buildRequestHeaders(authInfo),
75+
},
76+
);
77+
const content = response.data.ai_feature_output;
78+
79+
const responseData: SuggestedIssuesResponse = {
80+
suggestedIssues: content.suggested_issues.map((issue: any) => ({
81+
issueType: issue.issue_type,
82+
fieldValues: {
83+
summary: issue.field_values.Summary,
84+
description: issue.field_values.Description,
85+
},
86+
})),
87+
};
88+
89+
if (!responseData.suggestedIssues || responseData.suggestedIssues.length === 0) {
90+
throw new Error('No suggested issues found');
91+
}
92+
93+
return responseData;
94+
} catch (error) {
95+
console.error('Error fetching issue suggestions:', error);
96+
throw error;
97+
}
98+
};
99+
100+
const buildRequestBody = (prompt: string): any => ({
101+
ai_feature_input: {
102+
source: 'SLACK',
103+
locale: 'en-US',
104+
context: {
105+
primary_message: {
106+
text: prompt,
107+
sender: '',
108+
timestamp: '',
109+
},
110+
},
111+
suggested_issues_config: {
112+
max_issues: 1,
113+
suggested_issue_field_types: [
114+
{
115+
issue_type: 'Task',
116+
fields: [
117+
{
118+
field_name: 'Summary',
119+
field_type: 'Short-Text',
120+
},
121+
{
122+
field_name: 'Description',
123+
field_type: 'Paragraph',
124+
},
125+
],
126+
},
127+
],
128+
},
129+
},
130+
});
131+
132+
const buildRequestHeaders = (authInfo: BasicAuthInfo): any => ({
133+
'Content-Type': 'application/json;charset=UTF-8',
134+
Accept: 'application/json;charset=UTF-8',
135+
'X-Experience-Id': 'ai-issue-create-slack',
136+
'X-Product': ProductJira,
137+
Authorization: 'Basic ' + Buffer.from(`${authInfo.username}:${authInfo.password}`).toString('base64'),
138+
});

src/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export function registerCommands(vscodeContext: ExtensionContext) {
171171
}
172172
},
173173
),
174-
commands.registerCommand(Commands.CreateIssue, (data: any, source?: string) => createIssue(data, source)),
174+
commands.registerCommand(Commands.CreateIssue, async (data: any, source?: string) => createIssue(data, source)),
175175
commands.registerCommand(
176176
Commands.ShowIssue,
177177
async (issueOrKeyAndSite: MinimalIssueOrKeyAndSite<DetailedSiteInfo>) => await showIssue(issueOrKeyAndSite),

src/commands/jira/createIssue.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,70 @@ import { Position, Range, Uri, ViewColumn, window, workspace, WorkspaceEdit } fr
33
import { startIssueCreationEvent } from '../../analytics';
44
import { ProductJira } from '../../atlclients/authInfo';
55
import { WorkspaceRepo } from '../../bitbucket/model';
6+
import { SimplifiedTodoIssueData } from '../../config/model';
67
import { Container } from '../../container';
8+
import { Logger } from '../../logger';
79
import { CommentData } from '../../webviews/createIssueWebview';
10+
import { IssueSuggestionManager } from './issueSuggestionManager';
811

912
export interface TodoIssueData {
1013
summary: string;
1114
uri: Uri;
1215
insertionPoint: Position;
16+
context: string;
1317
}
1418

15-
export function createIssue(data: Uri | TodoIssueData | undefined, source?: string) {
19+
const simplify = (data: TodoIssueData): SimplifiedTodoIssueData => {
20+
return {
21+
summary: data.summary,
22+
context: data.context,
23+
position: {
24+
line: data.insertionPoint.line,
25+
character: data.insertionPoint.character,
26+
},
27+
uri: data.uri.toString(),
28+
};
29+
};
30+
31+
export async function createIssue(data: Uri | TodoIssueData | undefined, source?: string) {
1632
if (isTodoIssueData(data)) {
17-
const partialIssue = {
18-
summary: data.summary,
19-
description: descriptionForUri(data.uri),
20-
uri: data.uri,
21-
position: data.insertionPoint,
22-
onCreated: annotateComment,
23-
};
24-
Container.createIssueWebview.createOrShow(ViewColumn.Beside, partialIssue);
33+
const settings = await IssueSuggestionManager.buildSettings();
34+
const todoData = simplify(data);
35+
36+
await Container.createIssueWebview.createOrShow(
37+
ViewColumn.Beside,
38+
{
39+
description: descriptionForUri(data.uri),
40+
uri: data.uri,
41+
position: data.insertionPoint,
42+
onCreated: annotateComment,
43+
},
44+
settings,
45+
todoData,
46+
);
47+
48+
try {
49+
const suggestionManager = new IssueSuggestionManager(settings);
50+
51+
await suggestionManager.generate(todoData).then(async (suggestion) => {
52+
await Container.createIssueWebview.fastUpdateFields({
53+
summary: suggestion.summary,
54+
description: suggestion.description,
55+
});
56+
});
57+
} catch (error) {
58+
// The view is already created with legacy logic, do nothing
59+
Logger.error(error, 'Error generating issue suggestion settings');
60+
}
61+
2562
startIssueCreationEvent('todoComment', ProductJira).then((e) => {
2663
Container.analyticsClient.sendTrackEvent(e);
2764
});
65+
2866
return;
29-
} else if (isUri(data) && data.scheme === 'file') {
67+
}
68+
69+
if (isUri(data) && data.scheme === 'file') {
3070
Container.createIssueWebview.createOrShow(ViewColumn.Active, { description: descriptionForUri(data) });
3171
startIssueCreationEvent('contextMenu', ProductJira).then((e) => {
3272
Container.analyticsClient.sendTrackEvent(e);

0 commit comments

Comments
 (0)