diff --git a/clients/vscode/package.json b/clients/vscode/package.json index 8d70d9bbfec9..8b70a0a07b3d 100644 --- a/clients/vscode/package.json +++ b/clients/vscode/package.json @@ -28,7 +28,8 @@ "Other" ], "activationEvents": [ - "onStartupFinished" + "onStartupFinished", + "onView:chatView" ], "main": "./dist/node/extension.js", "browser": "./dist/web/extension.js", @@ -241,7 +242,26 @@ "key": "escape", "when": "inlineSuggestionVisible" } - ] + ], + "viewsContainers": { + "activitybar": [ + { + "id": "chatView", + "title": "Tabby", + "icon": "assets/logo.png" + } + ] + }, + "views": { + "chatView": [ + { + "type": "webview", + "id": "tabby.chatView", + "name": "", + "visibility": "visible" + } + ] + } }, "scripts": { "prebuild": "cd ../tabby-agent && yarn build && cd ../tabby-chat-panel && yarn build:vscode", diff --git a/clients/vscode/src/ChatViewProvider.ts b/clients/vscode/src/ChatViewProvider.ts new file mode 100644 index 000000000000..c92e3edb490b --- /dev/null +++ b/clients/vscode/src/ChatViewProvider.ts @@ -0,0 +1,206 @@ +import { ExtensionContext, Disposable, Webview, WebviewPanel, window, Uri, ViewColumn, WebviewViewProvider, WebviewView, TextDocument } from "vscode"; +import { getUri, getNonce } from "./utils"; + +import { createAgentInstance, disposeAgentInstance } from "./agent"; + +/** + * This class manages the state and behavior of HelloWorld webview panels. + * + * It contains all the data and methods for: + * + * - Creating and rendering HelloWorld webview panels + * - Properly cleaning up and disposing of webview resources when the panel is closed + * - Setting the HTML (and by proxy CSS/JavaScript) content of the webview panel + * - Setting message listeners so data can be passed between the webview and extension + */ +export class ChatViewProvider implements WebviewViewProvider { + _view?: WebviewView; + _doc?: TextDocument; + private readonly _extensionUri: Uri + private readonly _context: ExtensionContext + + // public static currentPanel: ChatViewPanel | undefined; + // private readonly _panel: WebviewPanel; + // private _disposables: Disposable[] = []; + + /** + * The ChatViewPanel class private constructor (called only from the render method). + * + * @param panel A reference to the webview panel + * @param extensionUri The URI of the directory containing the extension + */ + // private constructor(panel: WebviewPanel, extensionUri: Uri) { + // this._panel = panel; + + // // Set an event listener to listen for when the panel is disposed (i.e. when the user closes + // // the panel or when the panel is closed programmatically) + // this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + // // Set the HTML content for the webview panel + // this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri); + + // // Set an event listener to listen for messages passed from the webview context + // this._setWebviewMessageListener(this._panel.webview); + // } + + constructor(context: ExtensionContext) { + this._extensionUri = context.extensionUri + this._context = context; + } + + /** + * Renders the current webview panel if it exists otherwise a new webview panel + * will be created and displayed. + * + * @param extensionUri The URI of the directory containing the extension. + */ + // public static render(extensionUri: Uri) { + // if (ChatViewPanel.currentPanel) { + // // If the webview panel already exists reveal it + // ChatViewPanel.currentPanel._panel.reveal(ViewColumn.One); + // } else { + // // If a webview panel does not already exist create and show a new one + // const panel = window.createWebviewPanel( + // // Panel view type + // "showHelloWorld", + // // Panel title + // "Hello World", + // // The editor column the panel should be displayed in + // ViewColumn.One, + // // Extra panel configurations + // { + // // Enable JavaScript in the webview + // enableScripts: true, + // // Restrict the webview to only load resources from the `out` and `webview-ui/build` directories + // localResourceRoots: [Uri.joinPath(extensionUri, "out"), Uri.joinPath(extensionUri, "webview/build")], + // } + // ); + + // ChatViewPanel.currentPanel = new ChatViewPanel(panel, extensionUri); + // } + // } + + public async resolveWebviewView(webviewView: WebviewView) { + this._view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionUri], + }; + webviewView.webview.html = await this._getWebviewContent(webviewView.webview, this._extensionUri); + webviewView.webview.onDidReceiveMessage((data) => { + console.log('onDidReceiveMessage data', data) + switch (data.command) { + case 'explainThis': { + const { text, language } = data; + console.log('text', text) + console.log('language', language) + return; + } + } + }); + console.log('resolveWebviewView this._view', this._view) + +} + + /** + * Cleans up and disposes of webview resources when the webview panel is closed. + */ + // public dispose() { + // ChatViewPanel.currentPanel = undefined; + + // // Dispose of the current webview panel + // this._panel.dispose(); + + // // Dispose of all disposables (i.e. commands) for the current webview panel + // while (this._disposables.length) { + // const disposable = this._disposables.pop(); + // if (disposable) { + // disposable.dispose(); + // } + // } + // } + + /** + * Defines and returns the HTML that should be rendered within the webview panel. + * + * @remarks This is also the place where references to the React webview build files + * are created and inserted into the webview HTML. + * + * @param webview A reference to the extension webview + * @param extensionUri The URI of the directory containing the extension + * @returns A template string literal containing the HTML that should be + * rendered within the webview panel + */ + private async _getWebviewContent(webview: Webview, extensionUri: Uri) { + const agent = await createAgentInstance(this._context); + const { server } = agent.getConfig() + const scriptUri = getUri(webview, extensionUri, ['webview', "index.js"]); + // Tip: Install the es6-string-html VS Code extension to enable code highlighting below + + console.log(server.endpoint) + return /*html*/ ` + + + + + + Tabby + + + +
+ + + + + + `; + } + + /** + * Sets up an event listener to listen for messages passed from the webview context and + * executes code based on the message that is recieved. + * + * @param webview A reference to the extension webview + * @param context A reference to the extension context + */ + // private _setWebviewMessageListener(webview: Webview) { + // webview.onDidReceiveMessage( + // (message: any) => { + // const command = message.command; + // const text = message.text; + + // switch (command) { + // case "hello": + // // Code that should run in response to the hello message command + // window.showInformationMessage(text); + // return; + // // Add more switch case statements here as more webview message commands + // // are created within the webview context (i.e. inside media/main.js) + // } + // }, + // undefined, + // this._disposables + // ); + // } + + public revive(panel: WebviewView) { + this._view = panel; + } + + public postMessage (message: any) { + if (this._view) { + console.log('this._view exist') + console.log('this._view?.webview.', this._view?.webview) + this._view?.webview.postMessage(message) + } + } +} \ No newline at end of file diff --git a/clients/vscode/src/extension.ts b/clients/vscode/src/extension.ts index 36ccc72266f6..5e1762002080 100644 --- a/clients/vscode/src/extension.ts +++ b/clients/vscode/src/extension.ts @@ -1,12 +1,13 @@ // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below -import { ExtensionContext, commands, languages, workspace } from "vscode"; +import { ExtensionContext, commands, languages, workspace, window } from "vscode"; import { getLogger } from "./logger"; import { createAgentInstance, disposeAgentInstance } from "./agent"; import { tabbyCommands } from "./commands"; import { TabbyCompletionProvider } from "./TabbyCompletionProvider"; import { TabbyStatusBarItem } from "./TabbyStatusBarItem"; import { RecentlyChangedCodeSearch } from "./RecentlyChangedCodeSearch"; +import { ChatViewProvider } from "./ChatViewProvider"; const logger = getLogger(); @@ -39,6 +40,18 @@ export async function activate(context: ExtensionContext) { const statusBarItem = new TabbyStatusBarItem(context, completionProvider); context.subscriptions.push(statusBarItem.register()); + // Register the Sidebar Panel + const chatViewProvider = new ChatViewProvider(context); + context.subscriptions.push( + window.registerWebviewViewProvider( + "tabby.chatView", + chatViewProvider, + { + webviewOptions: { retainContextWhenHidden: true }, + }, + ) + ); + context.subscriptions.push(...tabbyCommands(context, completionProvider, statusBarItem)); const updateIsChatEnabledContextVariable = () => { diff --git a/clients/vscode/src/utils.ts b/clients/vscode/src/utils.ts index 0cd9eb308e70..049a9ae459f6 100644 --- a/clients/vscode/src/utils.ts +++ b/clients/vscode/src/utils.ts @@ -1,4 +1,4 @@ -import { commands, Position, Range, SemanticTokens, SemanticTokensLegend, TextDocument } from "vscode"; +import { commands, Position, Range, SemanticTokens, SemanticTokensLegend, TextDocument, Uri, Webview } from "vscode"; export type SemanticSymbolInfo = { position: Position; @@ -133,3 +133,23 @@ export function extractNonReservedWordList(text: string): string { ...new Set(text.match(re)?.filter((symbol) => symbol.length > 2 && !reservedKeywords.includes(symbol))).values(), ].join(" "); } + +/** + * This function is primarily used to help enforce content security + * policies for resources/scripts being executed in a webview context. + */ +export function getNonce() { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +/** + * A helper function which will get the webview URI of a given file or resource. + */ +export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); +} diff --git a/yarn.lock b/yarn.lock index 4101f288a88b..e36547362c79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3888,7 +3888,16 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3920,7 +3929,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -4433,7 +4449,16 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==