Skip to content

Commit 98cb993

Browse files
igardevigardevggerganov
authored
Chat with AI added with with webui (#38)
* Implement ask ai - open ai chat window from local model with or without project context - from menu or with Ctrl+; or Ctrl+Shift+; (with project context). New property endpoint_chat for the chat server endpoint. * Add menu items for showing ask AI windows * Reduce menu items - remove those with 0.5B models * Remove default value for self-sert file * various fixes * Rename ask-ai to chat-with-ai, clear the sent chunks to ai in case the window is closed. --------- Co-authored-by: igardev <[email protected]> Co-authored-by: Georgi Gerganov <[email protected]>
1 parent 41966cd commit 98cb993

File tree

9 files changed

+409
-72
lines changed

9 files changed

+409
-72
lines changed

package.json

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "llama-vscode",
33
"displayName": "llama-vscode",
44
"description": "Local LLM-assisted text completion using llama.cpp",
5-
"version": "0.0.8",
5+
"version": "0.0.9-beta-3",
66
"publisher": "ggml-org",
77
"repository": "https://github.com/ggml-org/llama.vscode",
88
"engines": {
@@ -60,6 +60,14 @@
6060
{
6161
"command": "extension.showMenu",
6262
"title": "Show Menu"
63+
},
64+
{
65+
"command": "extension.askAi",
66+
"title": "Ask AI"
67+
},
68+
{
69+
"command": "extension.askAiWithContext",
70+
"title": "Ask AI With Context"
6371
}
6472
],
6573
"keybindings": [
@@ -108,21 +116,41 @@
108116
"command": "extension.showMenu",
109117
"key": "ctrl+shift+m",
110118
"when": "true"
119+
},
120+
{
121+
"command": "extension.askAi",
122+
"key": "ctrl+;",
123+
"when": "editorTextFocus"
124+
},
125+
{
126+
"command": "extension.askAiWithContext",
127+
"key": "ctrl+Shift+;",
128+
"when": "editorTextFocus"
111129
}
112130
],
113131
"configuration": {
114132
"type": "object",
115133
"title": "llama.vscode Configuration",
116134
"properties": {
117-
"llama-vscode.launch_cmd": {
135+
"llama-vscode.launch_completion": {
118136
"type": "string",
119137
"default": "",
120-
"description": "Shell command executed from menu"
138+
"description": "Shell command for starting fim llama.cpp server, executed from the menu"
139+
},
140+
"llama-vscode.launch_chat": {
141+
"type": "string",
142+
"default": "",
143+
"description": "Shell command for starting chat llama.cpp server, executed from the menu"
121144
},
122145
"llama-vscode.endpoint": {
123146
"type": "string",
124147
"default": "http://127.0.0.1:8012",
125-
"description": "The URL to be used by the extension."
148+
"description": "The URL to be used by the extension for code completion."
149+
},
150+
"llama-vscode.endpoint_chat": {
151+
"type": "string",
152+
"default": "http://127.0.0.1:8011",
153+
"description": "The URL to be used by the extension for chat with ai."
126154
},
127155
"llama-vscode.auto": {
128156
"type": "boolean",
@@ -134,6 +162,11 @@
134162
"default": "",
135163
"description": "llama.cpp server api key or OpenAI endpoint API key (optional)"
136164
},
165+
"llama-vscode.self_signed_certificate": {
166+
"type": "string",
167+
"default": "",
168+
"description": "self-signed certificate file - path/to/cert.pem"
169+
},
137170
"llama-vscode.n_prefix": {
138171
"type": "number",
139172
"default": 256,

src/application.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {Statusbar} from "./statusbar";
77
import {Menu} from "./menu";
88
import {Completion} from "./completion";
99
import {Logger} from "./logger";
10+
import { ChatWithAi } from "./chat-with-ai";
1011

1112
export class Application {
1213
private static instance: Application;
@@ -19,6 +20,7 @@ export class Application {
1920
public menu: Menu
2021
public completion: Completion
2122
public logger: Logger
23+
public askAi: ChatWithAi
2224

2325
private constructor() {
2426
this.extConfig = new Configuration()
@@ -30,6 +32,7 @@ export class Application {
3032
this.menu = new Menu(this)
3133
this.completion = new Completion(this)
3234
this.logger = new Logger(this)
35+
this.askAi = new ChatWithAi(this)
3336
}
3437

3538
public static getInstance(): Application {

src/architect.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// TODO
2-
// Profiling
3-
// - Търсенето в кеша при 250 елемента и 49 символа отнема 1/5 милисекунда => може по-голям кеш, може търсене до началото на реда
4-
// - ShowInfo < 1/10 мс
2+
// Превод на меню и попъп съобщения на поддържаните езици + улесни превеждането - с ИИ.
53
// Да не премигва при избор само на ред или дума (върни частично проверката за съвпадение с последния рекуест?)
4+
// Идеи
5+
// - Използване на агенти (?)
6+
// - използване lSP
7+
// - използване на MCP
68
import * as vscode from 'vscode';
79
import {Application} from "./application";
810

@@ -78,7 +80,7 @@ export class Architect {
7880
const showMenuCommand = vscode.commands.registerCommand(
7981
'extension.showMenu',
8082
async () => {
81-
await this.app.menu.showMenu();
83+
await this.app.menu.showMenu(context);
8284
}
8385
);
8486
context.subscriptions.push(showMenuCommand);
@@ -89,7 +91,6 @@ export class Architect {
8991
const rungBufferUpdateDisposable = {
9092
dispose: () => {
9193
clearInterval(ringBufferIntervalId);
92-
console.log('Periodic Task Extension has been deactivated. Interval cleared.');
9394
}
9495
};
9596
context.subscriptions.push(rungBufferUpdateDisposable);
@@ -203,8 +204,50 @@ export class Architect {
203204
this.app.statusbar.initializeStatusBar();
204205
this.app.statusbar.registerEventListeners(context);
205206

206-
context.subscriptions.push(
207-
vscode.commands.registerCommand('llama-vscode.showMenu', this.app.menu.showMenu)
207+
context.subscriptions.push(vscode.commands.registerCommand('llama-vscode.showMenu', async () => {
208+
await this.app.menu.showMenu(context);
209+
})
208210
);
209211
}
212+
213+
registerCommandAskAi = (context: vscode.ExtensionContext) => {
214+
const triggerAskAiDisposable = vscode.commands.registerCommand('extension.askAi', async () => {
215+
if (!vscode.window.activeTextEditor) {
216+
vscode.window.showErrorMessage('No active editor!');
217+
return;
218+
}
219+
220+
if (!this.app.extConfig.endpoint_chat) return;
221+
222+
this.app.askAi.showChatWithAi(false, context);
223+
});
224+
context.subscriptions.push(triggerAskAiDisposable);
225+
}
226+
227+
registerCommandAskAiWithContext = (context: vscode.ExtensionContext) => {
228+
const triggerAskAiDisposable = vscode.commands.registerCommand('extension.askAiWithContext', async () => {
229+
if (!vscode.window.activeTextEditor) {
230+
vscode.window.showErrorMessage('No active editor!');
231+
return;
232+
}
233+
234+
if (!this.app.extConfig.endpoint_chat) return;
235+
236+
const editor = vscode.window.activeTextEditor;
237+
if (editor) {
238+
// if editor is active in the UI, pick a chunk before sending extra context to the ai
239+
let activeDocument = editor.document;
240+
const selection = editor.selection;
241+
const cursorPosition = selection.active;
242+
// setTimeout(async () => {
243+
this.app.extraContext.pickChunkAroundCursor(cursorPosition.line, activeDocument);
244+
// }, 0);
245+
// Ensure ring chunks buffer will be updated
246+
this.app.extraContext.lastComplStartTime = Date.now() - this.app.extConfig.RING_UPDATE_MIN_TIME_LAST_COMPL - 1
247+
this.app.extraContext.periodicRingBufferUpdate()
248+
}
249+
this.app.askAi.showChatWithAi(true, context);
250+
});
251+
context.subscriptions.push(triggerAskAiDisposable);
252+
}
210253
}

src/chat-with-ai.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {Application} from "./application";
2+
import * as vscode from 'vscode';
3+
4+
export class ChatWithAi {
5+
private app: Application
6+
private askAiPanel: vscode.WebviewPanel | undefined
7+
private askAiWithContextPanel: vscode.WebviewPanel | undefined
8+
private lastActiveEditor: vscode.TextEditor | undefined;
9+
private sentContextChunks: string[] = []
10+
11+
constructor(application: Application) {
12+
this.app = application;
13+
}
14+
15+
showChatWithAi = (withContext: boolean, context: vscode.ExtensionContext) => {
16+
const editor = vscode.window.activeTextEditor;
17+
let webviewIdentifier = 'htmlChatWithAiViewer'
18+
let panelTitle = 'Chat With AI'
19+
let aiPanel = this.askAiPanel
20+
let extraCont = "";
21+
if (withContext){
22+
aiPanel = this.askAiWithContextPanel
23+
if (!aiPanel) this.sentContextChunks = []
24+
webviewIdentifier = 'htmlChatWithAiWithContextViewer'
25+
let chunksToSend = this.app.extraContext.chunks.filter((_, index) => !this.sentContextChunks.includes(this.app.extraContext.chunksHash[index]));
26+
let chunksToSendHash = this.app.extraContext.chunksHash.filter((item) => !this.sentContextChunks.includes(item));
27+
if (chunksToSend.length > 0) extraCont = "Here are pieces of code from different files of the project: \n" + chunksToSend.reduce((accumulator, currentValue) => accumulator + "\nFile Name: " + currentValue.filename + "\nText:\n" + currentValue.text + "\n\n" , "");
28+
this.sentContextChunks.push(...chunksToSendHash)
29+
panelTitle = "Chat With AI With Project Context"
30+
}
31+
let selectedText = ""
32+
if (editor) {
33+
selectedText = editor.document.getText(editor.selection);
34+
if (selectedText.length > 0) selectedText = "Explain the following source code: " + selectedText
35+
}
36+
if (!aiPanel) {
37+
aiPanel = vscode.window.createWebviewPanel(
38+
webviewIdentifier,
39+
panelTitle,
40+
vscode.ViewColumn.Three, // Editor column to show the Webview
41+
{
42+
enableScripts: true, // Allow JavaScript execution
43+
retainContextWhenHidden: true,
44+
}
45+
);
46+
if (withContext) this.askAiWithContextPanel = aiPanel;
47+
else this.askAiPanel = aiPanel;
48+
49+
if (aiPanel) context.subscriptions.push(aiPanel);
50+
const targetUrl = this.app.extConfig.endpoint_chat + "/";
51+
aiPanel.webview.html = this.getWebviewContent(targetUrl);
52+
aiPanel.onDidDispose(() => {
53+
if (withContext) this.askAiWithContextPanel = undefined
54+
else this.askAiPanel = undefined
55+
});
56+
aiPanel.webview.onDidReceiveMessage((message) => {
57+
if (message.command === 'escapePressed') {
58+
this.focusEditor();
59+
} else if (message.command === 'jsAction') {
60+
// console.log("onDidReceiveMessage: " + message.text);
61+
}
62+
});
63+
// Wait for the page to load before sending message
64+
setTimeout(async () => {
65+
if (aiPanel) aiPanel.webview.postMessage({ command: 'setText', text: selectedText, context: extraCont });
66+
}, 1000);
67+
} else {
68+
aiPanel.reveal();
69+
this.lastActiveEditor = editor;
70+
// Wait for the page to load before sending message
71+
setTimeout(async () => {
72+
if (aiPanel) aiPanel.webview.postMessage({ command: 'setText', text: selectedText, context: extraCont });
73+
}, 500);
74+
}
75+
}
76+
77+
focusEditor = () => {
78+
if (this.lastActiveEditor) {
79+
vscode.window.showTextDocument(this.lastActiveEditor.document, this.lastActiveEditor.viewColumn, false);
80+
}
81+
}
82+
83+
getWebviewContent = (url: string): string => {
84+
return `
85+
<!DOCTYPE html>
86+
<html lang="en">
87+
<head>
88+
<meta charset="UTF-8">
89+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
90+
<title>llama.cpp server UI</title>
91+
<script>
92+
// Initialize the VS Code API
93+
const vscode = acquireVsCodeApi();
94+
vscode.postMessage({ command: 'jsAction', text: 'vscode javascript object created' });
95+
96+
// Listen for messages from the extension
97+
window.addEventListener('message', (event) => {
98+
vscode.postMessage({ command: 'jsAction', text: 'message received' });
99+
100+
const { command, text, context } = event.data; // Extract the command and text from the event
101+
if (command === 'setText') {
102+
vscode.postMessage({ command: 'jsAction', text: 'command setText received' });
103+
104+
const iframe = document.getElementById('askAiIframe');
105+
if (iframe) {
106+
vscode.postMessage({ command: 'jsAction', text: 'askAiIframe obtained' });
107+
iframe.contentWindow.postMessage({ command: 'setText', text: text, context: context }, '*');
108+
vscode.postMessage({ command: 'jsAction', text: text });
109+
}
110+
}
111+
if (command === 'escapePressed') {
112+
vscode.postMessage({ command: 'jsAction', text: 'command escape pressed' });
113+
vscode.postMessage({ command: 'escapePressed' });
114+
}
115+
if (command === 'jsAction') {
116+
vscode.postMessage({ command: 'jsAction', text: text });
117+
}
118+
});
119+
120+
// Listen for key events in the iframe
121+
window.addEventListener('keydown', (event) => {
122+
vscode.postMessage({ command: 'jsAction', text: 'keydown event received' });
123+
if (event.key === 'Escape') {
124+
// Send a message to the extension when Escape is pressed
125+
vscode.postMessage({ command: 'escapePressed', text: "" });
126+
vscode.postMessage({ command: 'jsAction', text: "Escabe key pressed..." });
127+
}
128+
});
129+
</script>
130+
<style>
131+
body, html {
132+
margin: 0;
133+
padding: 0;
134+
width: 100%;
135+
height: 100%;
136+
overflow: hidden;
137+
}
138+
iframe {
139+
width: 100%;
140+
height: 100%;
141+
border: none;
142+
}
143+
</style>
144+
</head>
145+
<body>
146+
<iframe src="${url}" id="askAiIframe"></iframe>
147+
</body>
148+
</html>
149+
`;
150+
}
151+
152+
}

0 commit comments

Comments
 (0)