Skip to content

Commit 96c04a8

Browse files
WIP
1 parent 2bc1f5f commit 96c04a8

File tree

10 files changed

+194
-128
lines changed

10 files changed

+194
-128
lines changed

src/atlclients/issueBuilder.ts

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,38 @@ export type SuggestedIssuesResponse = {
1616
suggestedIssues: SuggestedIssue[];
1717
};
1818

19-
export const findCloudSiteWithApiKey = async (): Promise<DetailedSiteInfo | null> => {
20-
const sites = await Promise.all(
21-
Container.siteManager.getSitesAvailable(ProductJira).map(async (site) => {
22-
if (!site.host.endsWith('.atlassian.net')) {
23-
return null;
24-
}
25-
26-
const authInfo = await Container.credentialManager.getAuthInfo(site);
27-
if (!authInfo || !isBasicAuthInfo(authInfo)) {
28-
return null;
29-
}
30-
31-
return site;
32-
}),
33-
).then((results) => results.filter(Boolean));
19+
type SiteId = string;
20+
21+
export const isSiteCloudWithApiKey = async (site?: DetailedSiteInfo | SiteId): Promise<boolean> => {
22+
const siteToCheck = typeof site === 'string' ? Container.siteManager.getSiteForId(ProductJira, site) : site;
23+
24+
if (!siteToCheck || !siteToCheck.host) {
25+
return false;
26+
}
27+
28+
if (!siteToCheck.host.endsWith('.atlassian.net')) {
29+
return false;
30+
}
31+
32+
const authInfo = await Container.credentialManager.getAuthInfo(siteToCheck);
33+
if (!authInfo || !isBasicAuthInfo(authInfo)) {
34+
return false;
35+
}
36+
37+
return true;
38+
};
39+
40+
export const findCloudSiteWithApiKey = async (): Promise<DetailedSiteInfo | undefined> => {
41+
const sites = (
42+
await Promise.all(
43+
Container.siteManager
44+
.getSitesAvailable(ProductJira)
45+
.map(async (x) => ((await isSiteCloudWithApiKey(x)) ? x : undefined)),
46+
)
47+
).filter((x) => x !== undefined) as DetailedSiteInfo[];
3448

3549
// Any site is fine, just need an API key
36-
const site = sites[0];
37-
return site;
50+
return sites.length > 0 ? sites[0] : undefined;
3851
};
3952

4053
export const fetchIssueSuggestions = async (prompt: string): Promise<SuggestedIssuesResponse> => {
@@ -53,13 +66,17 @@ export const fetchIssueSuggestions = async (prompt: string): Promise<SuggestedIs
5366
}
5467

5568
const response = await axiosInstance.post(
56-
`https://${site.host}/gateway/api/assist/chat/v1/invoke_agent`,
69+
`https://${site.host}/gateway/api/assist/api/ai/v2/ai-feature/jira/issue/source-type/conversation/suggestions`,
5770
{
58-
recipient_agent_named_id: 'ai_issue_create_agent',
59-
agent_input_context: {
60-
application: 'Slack',
71+
ai_feature_input: {
72+
source: 'SLACK',
73+
locale: 'en-US',
6174
context: {
62-
primary_message: { text: prompt },
75+
primary_message: {
76+
text: prompt,
77+
sender: '',
78+
timestamp: '',
79+
},
6380
},
6481
suggested_issues_config: {
6582
max_issues: 1,
@@ -92,7 +109,7 @@ export const fetchIssueSuggestions = async (prompt: string): Promise<SuggestedIs
92109
},
93110
},
94111
);
95-
const content = JSON.parse(response.data.message.content);
112+
const content = response.data.ai_feature_output;
96113

97114
const responseData: SuggestedIssuesResponse = {
98115
suggestedIssues: content.suggested_issues.map((issue: any) => ({

src/commands/jira/createIssue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function createIssue(data: Uri | TodoIssueData | undefined, source?
4949
const suggestionManager = new IssueSuggestionManager(settings);
5050

5151
await suggestionManager.generate(todoData).then(async (suggestion) => {
52-
await Container.createIssueWebview.forceUpdateFields({
52+
await Container.createIssueWebview.fastUpdateFields({
5353
summary: suggestion.summary,
5454
description: suggestion.description,
5555
});

src/commands/jira/issueSuggestionManager.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { FeatureFlagClient, Features } from 'src/util/featureFlags';
22
import { window, workspace } from 'vscode';
33

4-
import { fetchIssueSuggestions, findCloudSiteWithApiKey } from '../../atlclients/issueBuilder';
4+
import { fetchIssueSuggestions, isSiteCloudWithApiKey } from '../../atlclients/issueBuilder';
55
import { IssueSuggestionContextLevel, IssueSuggestionSettings, SimplifiedTodoIssueData } from '../../config/model';
66

77
export class IssueSuggestionManager {
88
static getSuggestionEnabled(): boolean {
9+
if (!FeatureFlagClient.checkGate(Features.EnableAiSuggestions)) {
10+
return false;
11+
}
12+
913
const config = workspace.getConfiguration('atlascode.issueSuggestion').get<boolean>('enabled');
10-
return config === true; // (as opposed to undefined)
14+
return Boolean(config);
1115
}
1216

1317
static getSuggestionContextLevel(): IssueSuggestionContextLevel {
@@ -19,7 +23,13 @@ export class IssueSuggestionManager {
1923
}
2024

2125
static async getSuggestionAvailable(): Promise<boolean> {
22-
return FeatureFlagClient.checkGate(Features.EnableAiSuggestions) && (await findCloudSiteWithApiKey()) !== null;
26+
const isFeatureEnabled = FeatureFlagClient.checkGate(Features.EnableAiSuggestions);
27+
28+
const selectedSite = workspace
29+
.getConfiguration('atlascode')
30+
.get<string>('jira.lastCreateSiteAndProject.siteId');
31+
32+
return Boolean(isFeatureEnabled && (await isSiteCloudWithApiKey(selectedSite)));
2333
}
2434

2535
static async buildSettings(): Promise<IssueSuggestionSettings> {
@@ -69,6 +79,7 @@ export class IssueSuggestionManager {
6979
error: 'Unable to fetch issue suggestions. Sorry!',
7080
};
7181
}
82+
7283
return {
7384
summary: issue.fieldValues.summary,
7485
description: issue.fieldValues.description,

src/webviews/components/App.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,7 +1042,14 @@ div[role='dialog'],
10421042
display: inline-block;
10431043
}
10441044

1045+
.ac-form-separator {
1046+
border: none;
1047+
border-top: 1px solid var(--vscode-settings-textInputBorder);
1048+
margin: 1rem 0;
1049+
}
1050+
10451051
.ac-inputField,
1052+
.ac-inputField:disabled,
10461053
.ac-inputField-inline,
10471054
.ac-textarea {
10481055
background: var(--vscode-input-background) !important;

src/webviews/components/aiCreateIssue/AISuggestionHeader.tsx

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
import Checkbox from '@atlaskit/checkbox';
22
import { HelperMessage } from '@atlaskit/form';
3-
import Select from '@atlaskit/select';
43
import React, { useEffect, useState } from 'react';
54
import { IssueSuggestionContextLevel, IssueSuggestionSettings } from 'src/config/model';
65

76
interface VsCodeApi {
87
postMessage(msg: {}): void;
98
}
109

11-
const contextLevelNames = new Map<string, string>([
12-
[IssueSuggestionContextLevel.CodeContext, 'Code context (Recommended)'],
13-
[IssueSuggestionContextLevel.TodoOnly, 'TODO text only'],
14-
]);
15-
1610
const AISuggestionHeader: React.FC<{
1711
vscodeApi: VsCodeApi;
1812
}> = ({ vscodeApi }) => {
@@ -25,7 +19,6 @@ const AISuggestionHeader: React.FC<{
2519
const { isAvailable, isEnabled, level } = suggestionSettings;
2620

2721
const [todoData, setTodoData] = useState(null);
28-
const [isLoading, setIsLoading] = useState(false);
2922

3023
useEffect(() => {
3124
if (!vscodeApi) {
@@ -48,6 +41,9 @@ const AISuggestionHeader: React.FC<{
4841
if (!todoData) {
4942
return;
5043
}
44+
window.postMessage({
45+
type: 'generateIssueSuggestions',
46+
});
5147
vscodeApi.postMessage({
5248
action: 'generateIssueSuggestions',
5349
todoData,
@@ -67,7 +63,6 @@ const AISuggestionHeader: React.FC<{
6763
};
6864
setSuggestionSettings(newState);
6965
updateIdeSettings(newState);
70-
setIsLoading(true);
7166
generateIssueSuggestions({ ...newState });
7267

7368
// update the footer
@@ -78,25 +73,11 @@ const AISuggestionHeader: React.FC<{
7873
});
7974
};
8075

81-
const handleSelectChange = (e: any) => {
82-
const newState = {
83-
isAvailable: isAvailable,
84-
isEnabled: isEnabled,
85-
level: e.value,
86-
};
87-
setSuggestionSettings(newState);
88-
updateIdeSettings(newState);
89-
setIsLoading(true);
90-
generateIssueSuggestions(newState);
91-
};
92-
9376
window.addEventListener('message', (event) => {
9477
const message = event.data;
9578
if (message.type === 'updateAiSettings') {
9679
setSuggestionSettings(message.newState);
9780
setTodoData(message.todoData);
98-
} else if (message.type === 'update') {
99-
setIsLoading(false);
10081
} else if (message.type === 'updateFeatureFlag') {
10182
console.log('updateFeatureFlag', message.value);
10283
setIsFeatureFlagEnabled(message.value);
@@ -109,40 +90,27 @@ const AISuggestionHeader: React.FC<{
10990

11091
return isAvailable ? (
11192
<div>
112-
<Checkbox label="Use AI to generate issue?" isChecked={isEnabled} onChange={handleCheckboxChange} />
113-
{isEnabled && (
114-
<div
115-
style={{
116-
display: 'flex',
117-
alignItems: 'center',
118-
gap: '10px',
119-
justifyContent: 'space-between',
120-
}}
121-
>
122-
<div style={{ flex: '1' }}>
123-
<HelperMessage>
124-
Please select the level of details you'd like to be used for the issue generation.
125-
</HelperMessage>
126-
</div>
127-
<div style={{ flex: '1' }}>
128-
<Select
129-
className="ac-form-select-container"
130-
classNamePrefix="ac-form-select"
131-
isSearchable={false}
132-
isLoading={isLoading}
133-
value={{
134-
label: contextLevelNames.get(level),
135-
value: level,
136-
}}
137-
options={Array.from(contextLevelNames.entries()).map(([value, label]) => ({
138-
value,
139-
label,
140-
}))}
141-
onChange={handleSelectChange}
142-
/>
143-
</div>
144-
</div>
145-
)}
93+
<hr className="ac-form-separator" />
94+
<Checkbox label="Use AI to create issue" isChecked={isEnabled} onChange={handleCheckboxChange} />
95+
<HelperMessage>
96+
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
97+
<span>AI will analyze your TODO and code context to suggest issue titles and descriptions.</span>
98+
<span title="AI suggestions use your code and TODO comments to generate issue details.">
99+
<svg
100+
width="16"
101+
height="16"
102+
viewBox="0 0 16 16"
103+
fill="none"
104+
aria-label="Info"
105+
style={{ verticalAlign: 'middle', cursor: 'pointer' }}
106+
>
107+
<circle cx="8" cy="8" r="7" stroke="#6B778C" strokeWidth="1.5" fill="#DEEBFF" />
108+
<rect x="7.25" y="7" width="1.5" height="4" rx="0.75" fill="#6B778C" />
109+
<rect x="7.25" y="4" width="1.5" height="1.5" rx="0.75" fill="#6B778C" />
110+
</svg>
111+
</span>
112+
</span>
113+
</HelperMessage>
146114
</div>
147115
) : (
148116
<HelperMessage>

src/webviews/components/editor/Editor.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ export function useEditor<T extends UserType>(props: {
170170
const view = useRef<EditorView | null>(null);
171171
const [content, setContent] = useState(props.value || '');
172172

173+
if (props.value !== content) {
174+
setContent(props.value);
175+
}
176+
173177
// Prevents unnecessary calls to fetchUsers
174178
const debouncedFetch = props.fetchUsers && debounce(props.fetchUsers, 1500, { leading: true, maxWait: 1 });
175179

@@ -191,6 +195,7 @@ export function useEditor<T extends UserType>(props: {
191195
return;
192196
}
193197
if (props.enabled) {
198+
view.current.state.doc = mdParser.parse(content);
194199
const slice = view.current.state.doc.slice(0);
195200
const tr = view.current.state.tr.replaceWith(0, slice?.size || 0, mdParser.parse(content));
196201
view.current.dispatch(tr);

0 commit comments

Comments
 (0)