Skip to content

Commit 554399f

Browse files
authored
feat: coding assistant UI updates for beta (#169)
feat: Add Ask Sourcery code lens feat: Add initial chat message refactor: drive showing messages in the UI from the binary
1 parent f664d38 commit 554399f

File tree

7 files changed

+246
-83
lines changed

7 files changed

+246
-83
lines changed

package.json

+14
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@
229229
"title": "Clear",
230230
"category": "Sourcery"
231231
},
232+
{
233+
"command": "sourcery.chat.ask",
234+
"title": "Ask Sourcery",
235+
"category": "Sourcery"
236+
},
232237
{
233238
"command": "sourcery.scan.selectLanguage",
234239
"title": "Select Language",
@@ -302,6 +307,11 @@
302307
"command": "sourcery.scan.open",
303308
"title": "Create Sourcery rule with this pattern",
304309
"when": "editorLangId==python && editorHasSelection"
310+
},
311+
{
312+
"command": "sourcery.chat.ask",
313+
"title": "Ask Sourcery",
314+
"when": "editorHasSelection && sourcery.features.coding_assistant"
305315
}
306316
],
307317
"sourcery.scans": [
@@ -345,6 +355,10 @@
345355
"command": "sourcery.chat.clearChat",
346356
"when": "false"
347357
},
358+
{
359+
"command": "sourcery.chat.ask",
360+
"when": "sourcery.features.coding_assistant"
361+
},
348362
{
349363
"command": "sourcery.scan.selectLanguage",
350364
"when": "false"

src/ask-sourcery.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as vscode from "vscode";
2+
import { Recipe, RecipeProvider } from "./recipes";
3+
import { ChatRequest } from "./chat";
4+
5+
export function askSourceryCommand(recipes: Recipe[], contextRange?) {
6+
showAskSourceryQuickPick(recipes).then((result: any) => {
7+
vscode.commands.executeCommand("sourcery.chat.focus").then(() => {
8+
let request: ChatRequest;
9+
if ("id" in result) {
10+
request = {
11+
type: "recipe_request",
12+
data: {
13+
kind: "recipe_request",
14+
name: result.label,
15+
id: result.id,
16+
},
17+
context_range: contextRange,
18+
};
19+
} else {
20+
request = {
21+
type: "chat_request",
22+
data: { kind: "user_message", message: result.label },
23+
context_range: contextRange,
24+
};
25+
}
26+
27+
vscode.commands.executeCommand("sourcery.chat_request", request);
28+
});
29+
});
30+
}
31+
32+
export function showAskSourceryQuickPick(recipes: Recipe[]) {
33+
return new Promise((resolve) => {
34+
const recipeNames = recipes.map((item) => item.name);
35+
const recipeItems = recipes.map((item) => ({
36+
label: item.name,
37+
id: item.id,
38+
}));
39+
40+
const quickPick = vscode.window.createQuickPick();
41+
quickPick.placeholder = "Ask any question or choose one of these recipes";
42+
quickPick.items = recipeItems;
43+
44+
quickPick.onDidAccept(() => {
45+
const selection = quickPick.activeItems[0];
46+
resolve(selection);
47+
quickPick.hide();
48+
});
49+
50+
quickPick.onDidChangeValue(() => {
51+
// add what the user has typed to the pick list as the first item
52+
if (!recipeNames.includes(quickPick.value)) {
53+
const newItems = [{ label: quickPick.value }, ...recipeItems];
54+
quickPick.items = newItems;
55+
}
56+
});
57+
quickPick.onDidHide(() => quickPick.dispose());
58+
quickPick.show();
59+
});
60+
}

src/chat.ts

+83-23
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,35 @@ marked.use(
1919
enum ChatResultOutcome {
2020
Success = "success",
2121
Error = "error",
22+
Finished = "finished",
23+
}
24+
25+
enum ChatResultRole {
26+
Assistant = "assistant",
27+
User = "user",
2228
}
2329

2430
type ChatResult = {
2531
outcome: ChatResultOutcome;
2632
textContent: string;
33+
role: ChatResultRole;
34+
};
35+
36+
export type ChatRequestData = {
37+
kind: "user_message";
38+
message: string;
39+
};
40+
41+
export type RecipeRequestData = {
42+
kind: "recipe_request";
43+
name: string;
44+
id: string;
2745
};
2846

2947
export type ChatRequest = {
30-
type: string;
31-
data: string;
48+
type: "recipe_request" | "chat_request";
49+
data: ChatRequestData | RecipeRequestData;
50+
context_range?: any;
3251
};
3352

3453
export class ChatProvider implements vscode.WebviewViewProvider {
@@ -38,7 +57,9 @@ export class ChatProvider implements vscode.WebviewViewProvider {
3857

3958
private _extensionUri: vscode.Uri;
4059

41-
private currentAssistantMessage: string = "";
60+
private _currentAssistantMessage: string = "";
61+
62+
private _unhandledMessages: ChatResult[] = [];
4263

4364
constructor(private _context: vscode.ExtensionContext) {
4465
this._extensionUri = _context.extensionUri;
@@ -71,50 +92,89 @@ export class ChatProvider implements vscode.WebviewViewProvider {
7192
webviewView.webview.onDidReceiveMessage(async (data: ChatRequest) => {
7293
switch (data.type) {
7394
case "chat_request": {
74-
this.currentAssistantMessage = "";
7595
vscode.commands.executeCommand("sourcery.chat_request", data);
7696
break;
7797
}
7898
}
7999
});
100+
if (this._unhandledMessages.length === 0) {
101+
vscode.commands.executeCommand("sourcery.initialise_chat");
102+
} else {
103+
while (this._unhandledMessages.length > 0) {
104+
const message = this._unhandledMessages.shift();
105+
this.addResult(message);
106+
}
107+
}
80108
}
81109

82110
public addResult(result: ChatResult) {
83-
// Send the whole message we've been streamed so far to the webview,
84-
// after converting from markdown to html
85-
86-
if (result.outcome === ChatResultOutcome.Success) {
87-
this.currentAssistantMessage += result.textContent;
111+
if (this._view) {
112+
if (result.role === ChatResultRole.User) {
113+
this.addUserResult(result);
114+
} else {
115+
this.addAssistantResult(result);
116+
}
88117
} else {
89-
this.currentAssistantMessage = result.textContent;
118+
this._unhandledMessages.push(result);
90119
}
120+
}
91121

92-
const rendered = marked(this.currentAssistantMessage, {
93-
gfm: true,
94-
breaks: true,
122+
private addUserResult(result: ChatResult) {
123+
this._view.webview.postMessage({
124+
command: "add_result",
125+
result: {
126+
role: result.role,
127+
outcome: result.outcome,
128+
textContent: result.textContent,
129+
},
95130
});
131+
}
96132

97-
const sanitized = sanitizeHtml(rendered, {
98-
allowedClasses: { span: false, code: false },
99-
});
133+
private addAssistantResult(result: ChatResult) {
134+
if (result.outcome === ChatResultOutcome.Finished) {
135+
this._currentAssistantMessage = "";
136+
this._view.webview.postMessage({ command: "assistant_finished" });
137+
return;
138+
}
139+
140+
if (result.outcome === ChatResultOutcome.Success) {
141+
this._currentAssistantMessage += result.textContent;
142+
} else {
143+
this._currentAssistantMessage = result.textContent;
144+
}
145+
146+
let sanitized = this.renderAssistantMessage(this._currentAssistantMessage);
100147

101148
this._view.webview.postMessage({
102149
command: "add_result",
103-
result: { outcome: result.outcome, textContent: sanitized },
150+
result: {
151+
role: result.role,
152+
outcome: result.outcome,
153+
textContent: sanitized,
154+
},
104155
});
105156
}
106157

107-
public executeRecipeRequest(message: string) {
108-
this.currentAssistantMessage = "";
109-
this._view.webview.postMessage({
110-
command: "recipe_request",
111-
result: message,
158+
private renderAssistantMessage(message: string) {
159+
// Send the whole message we've been streamed so far to the webview,
160+
// after converting from markdown to html
161+
162+
const rendered = marked(message, {
163+
gfm: true,
164+
breaks: true,
165+
mangle: false,
166+
headerIds: false,
167+
});
168+
169+
// Allow any classes on span and code blocks or highlightjs classes get removed
170+
return sanitizeHtml(rendered, {
171+
allowedClasses: { span: false, code: false },
112172
});
113173
}
114174

115175
public clearChat() {
116176
this._view.webview.postMessage({ command: "clear_chat" });
117-
this.currentAssistantMessage = "";
177+
this._currentAssistantMessage = "";
118178
}
119179

120180
private async _getHtmlForWebview(webview: vscode.Webview) {

src/extension.ts

+50-25
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import {
2727
LanguageClient,
2828
LanguageClientOptions,
2929
ServerOptions,
30+
URI,
3031
} from "vscode-languageclient/node";
3132
import { getHubSrc } from "./hub";
3233
import { RuleInputProvider } from "./rule-search";
3334
import { ScanResultProvider } from "./rule-search-results";
3435
import { ChatProvider, ChatRequest } from "./chat";
35-
import { RecipeProvider } from "./recipes";
36+
import { Recipe, RecipeProvider } from "./recipes";
37+
import { askSourceryCommand } from "./ask-sourcery";
3638

3739
function createLangServer(): LanguageClient {
3840
const token = workspace.getConfiguration("sourcery").get<string>("token");
@@ -172,7 +174,8 @@ function registerCommands(
172174
tree: ScanResultProvider,
173175
treeView: TreeView<TreeItem>,
174176
hubWebviewPanel: WebviewPanel,
175-
chatProvider: ChatProvider
177+
chatProvider: ChatProvider,
178+
recipeProvider: RecipeProvider
176179
) {
177180
context.subscriptions.push(
178181
vscode.window.registerWebviewViewProvider(
@@ -214,6 +217,13 @@ function registerCommands(
214217
})
215218
);
216219

220+
context.subscriptions.push(
221+
commands.registerCommand("sourcery.chat.ask", (arg?) => {
222+
let contextRange = arg && "start" in arg ? arg : null;
223+
askSourceryCommand(recipeProvider.recipes, contextRange);
224+
})
225+
);
226+
217227
context.subscriptions.push(
218228
commands.registerCommand("sourcery.scan.selectLanguage", () => {
219229
const items = ["python", "javascript"];
@@ -382,24 +392,30 @@ function registerCommands(
382392
)
383393
);
384394

395+
context.subscriptions.push(
396+
commands.registerCommand("sourcery.initialise_chat", () => {
397+
let request: ExecuteCommandParams = {
398+
command: "sourcery/chat/initialise",
399+
arguments: [],
400+
};
401+
languageClient.sendRequest(ExecuteCommandRequest.type, request);
402+
})
403+
);
404+
385405
context.subscriptions.push(
386406
commands.registerCommand(
387407
"sourcery.chat_request",
388408
(message: ChatRequest) => {
389-
const selectionLocation = getSelectionLocation();
390-
const activeEditor = window.activeTextEditor;
391-
let activeFile = undefined;
392-
if (activeEditor) {
393-
activeFile = activeEditor.document.uri;
394-
}
395-
const allFiles = [];
396-
for (const tabGroup of vscode.window.tabGroups.all) {
397-
for (const tab of tabGroup.tabs) {
398-
if (tab.input instanceof vscode.TabInputText) {
399-
allFiles.push(tab.input.uri);
400-
}
401-
}
409+
// Use the editor selection unless a range was passed through in
410+
// the message
411+
let selectionLocation = getSelectionLocation();
412+
if (message.context_range != null) {
413+
selectionLocation = {
414+
uri: selectionLocation.uri,
415+
range: message.context_range,
416+
};
402417
}
418+
let { activeFile, allFiles } = activeFiles();
403419

404420
let request: ExecuteCommandParams = {
405421
command: "sourcery/chat/request",
@@ -417,15 +433,6 @@ function registerCommands(
417433
)
418434
);
419435

420-
context.subscriptions.push(
421-
commands.registerCommand(
422-
"sourcery.recipe_request",
423-
(message: ChatRequest) => {
424-
chatProvider.executeRecipeRequest(message.data);
425-
}
426-
)
427-
);
428-
429436
context.subscriptions.push(
430437
commands.registerCommand(
431438
"sourcery.scan.rule",
@@ -561,7 +568,8 @@ export function activate(context: ExtensionContext) {
561568
tree,
562569
treeView,
563570
hubWebviewPanel,
564-
chatProvider
571+
chatProvider,
572+
recipeProvider
565573
);
566574

567575
showSourceryStatusBarItem(context);
@@ -588,3 +596,20 @@ function openDocument(document_path: string) {
588596
window.showTextDocument(doc);
589597
});
590598
}
599+
600+
function activeFiles(): { activeFile: URI | null; allFiles: URI[] } {
601+
const activeEditor = window.activeTextEditor;
602+
let activeFile = null;
603+
if (activeEditor) {
604+
activeFile = activeEditor.document.uri;
605+
}
606+
const allFiles = [];
607+
for (const tabGroup of vscode.window.tabGroups.all) {
608+
for (const tab of tabGroup.tabs) {
609+
if (tab.input instanceof vscode.TabInputText) {
610+
allFiles.push(tab.input.uri);
611+
}
612+
}
613+
}
614+
return { activeFile, allFiles };
615+
}

0 commit comments

Comments
 (0)