Starting a VSCode Extension UI For DBChat (Part 7)

Starting a VSCode Extension UI For DBChat (Part 7)


Hi there! I’m Shrijith Venkatrama, the founder of Hexmos. Right now, I’m building LiveAPI, a super-convenient tool that simplifies engineering workflows by generating awesome API docs from your code in minutes.

In this tutorial series, I am on a journey to build for myself DBChat – a simple tool for using AI chat to explore and evolve databases.

See previous posts to get more context:

  1. Building DBChat – Explore and Evolve Your DB with Simple Chat (Part 1)
  2. DBChat: Getting a Toy REPL Going in Golang (Part 2)
  3. DBChat Part 3 – Configure , Connect & Dump Databases
  4. Chat With Your DB via DBChat & Gemini (Part 4)
  5. The Language Server Protocol – Building DBChat (Part 5)
  6. Making DBChat VSCode Extension – Ping Pong With LSP Backend (Part 6)



A VSCode UI for Creating & Managing DB Connections

Our first task is to clearly define what exactly we want.

Ultimately, with DBChat – we aim to put a nice chat interface for talking to databases to explore & evolve them.

And from typical cases I’ve seen, Cursor (the most popular dev chat extension) appears on the right side of the screen.

So to keep things distinct from Cursor, I’ve decided to put DBChat on the left sidebar with a DBChat button.

On clicking this button (or via cmd search or keyboard shortcut), one can bring up the chat view.

However, a “chat view” is meaningless without databases and connections to them.

In Part 4 of this series, we introduced ~/.dbchat.toml. The file will list all the DB connections one may use during chat as follows:

# DBChat Sample Configuration File
# Copy this file to ~/.dbchat.toml and modify as needed

[connections]
# Format: name = "connection_string"
local = "postgresql://postgres:postgres@localhost:5432/postgres"
liveapi = "postgresql://user:pwd@ip:5432/db_name" 

[llm]
gemini_key = "the_key"
Enter fullscreen mode

Exit fullscreen mode

Now we will want to build an extension UI to manipulate the [connections] section.

We will have two parts to the UI:

  1. A list of database connections (already added)
  2. A Plus “+” button, and an “Add connection” page



The New package.json – Register View Container, View and Menu

In the following package.json, the sections to take not of are:

  1. viewsContainers – This is where we list the sidebar button in the activity bar
  2. views – This is where we define the panel associated with the button from (1)
  3. menus – Finally, at the top of the new panel – we want a plus “+” button.
{
  "name": "dbchat",
  "displayName": "DBChat",
  "description": "Explore and Evolve Databases With Simple AI Chat",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.96.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [
    "onCommand:dbchat.ping",
    "onView:dbchat.chatPanel"
  ],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "dbchat.ping",
        "title": "DBChat: Ping"
      },
      {
        "command": "dbchat.addConnection",
        "title": "Add Database Connection",
        "icon": "$(add)"
      }
    ],
    "viewsContainers": {
      "activitybar": [
        {
          "id": "dbchat-sidebar",
          "title": "DB Chat",
          "icon": "resources/database.svg"
        }
      ]
    },
    "views": {
      "dbchat-sidebar": [
        {
          "type": "webview",
          "id": "dbchat.chatPanel",
          "name": "DB Chat",
          "icon": "resources/database.svg"
        }
      ]
    },
    "menus": {
      "view/title": [
        {
          "command": "dbchat.addConnection",
          "when": "view == dbchat.chatPanel",
          "group": "navigation"
        }
      ]
    }
  },
  "scripts": {
    "vscode:prepublish": "npm run package",
    "compile": "npm run check-types && npm run lint && node esbuild.js",
    "watch": "npm-run-all -p watch:*",
    "watch:esbuild": "node esbuild.js --watch",
    "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
    "package": "npm run check-types && npm run lint && node esbuild.js --production",
    "compile-tests": "tsc -p . --outDir out",
    "watch-tests": "tsc -p . -w --outDir out",
    "pretest": "npm run compile-tests && npm run compile && npm run lint",
    "check-types": "tsc --noEmit",
    "lint": "eslint src",
    "test": "vscode-test"
  },
  "devDependencies": {
    "@types/vscode": "^1.96.0",
    "@types/mocha": "^10.0.10",
    "@types/node": "20.x",
    "@typescript-eslint/eslint-plugin": "^8.17.0",
    "@typescript-eslint/parser": "^8.17.0",
    "eslint": "^9.16.0",
    "esbuild": "^0.24.0",
    "npm-run-all": "^4.1.5",
    "typescript": "^5.7.2",
    "@vscode/test-cli": "^0.0.10",
    "@vscode/test-electron": "^2.4.1"
  }
}
Enter fullscreen mode

Exit fullscreen mode



Extension Components Are Straightforward Simple HTML/CSS/JS Bundles

Once the foundations are set up, building an extension is not too different from building a web app.

We define a few components first:

(1) An empty main view:

        const mainView = `
            <div class="container">
                <div id="connectionsList">
                    <!-- Connections will be listed here -->
                    <div class="empty-state">No connections added yet</div>
                </div>
            </div>
        `;
Enter fullscreen mode

Exit fullscreen mode

(2) A form to add connections

        const connectionForm = `
            <div class="container">
                <form id="connectionForm">
                    <div class="form-group">
                        <label for="name">Connection Name:</label>
                        <input type="text" id="name" required>
                    </div>
                    <div class="form-group">
                        <label for="connectionString">Connection String:</label>
                        <input type="text" id="connectionString" required>
                    </div>
                    <div class="button-group">
                        <button type="submit">Save</button>
                        <button type="button" id="cancelButton">Cancel</button>
                    </div>
                </form>
            </div>
        `;
Enter fullscreen mode

Exit fullscreen mode

(3) A simple “switcher” between main vs add connection view:

                <body>
                    ${this._showingConnectionForm ? connectionForm : mainView}
                    <script>
                        (function() {
                            const vscode = acquireVsCodeApi();

                            if (document.getElementById('connectionForm')) {
                                document.getElementById('connectionForm').addEventListener('submit', (e) => {
                                    e.preventDefault();
                                    const name = document.getElementById('name').value;
                                    const connectionString = document.getElementById('connectionString').value;
                                    vscode.postMessage({ 
                                        command: 'saveConnection',
                                        name,
                                        connectionString
                                    });
                                });

                                document.getElementById('cancelButton').addEventListener('click', () => {
                                    vscode.postMessage({ command: 'cancel' });
                                });
                            }
                        })();
                    </script>
                </body>
Enter fullscreen mode

Exit fullscreen mode



Putting the Pieces Together

Once we have the components that makes up the larger experience, we use VSCode API glue to put them together into a single flow:

We define a DBChatPanel:

export class DBChatPanel {
    private static readonly viewType = 'dbchat.chatPanel';
    private readonly _view: vscode.WebviewView;
    private _showingConnectionForm: boolean = false;

    constructor(webviewView: vscode.WebviewView, context: vscode.ExtensionContext) {
        this._view = webviewView;
        this._view.webview.options = {
            enableScripts: true,
            localResourceRoots: []
        };

        // Set up the toolbar with the add button
        this._view.description = "Database connections";
        this._view.title = "DB Chat";
        this._view.titleDescription = "Database connections";

        // Register the add connection command
        const addConnectionCommand = vscode.commands.registerCommand('dbchat.addConnection', () => {
            this._showingConnectionForm = true;
            this._updateView();
        });

        // Add to extension subscriptions for proper cleanup
        context.subscriptions.push(addConnectionCommand);

        // Handle messages from the webview
        this._view.webview.onDidReceiveMessage(
            async (message) => {
                switch (message.command) {
                    case 'saveConnection':
                        await this._saveConnection(message.name, message.connectionString);
                        this._showingConnectionForm = false;
                        this._updateView();
                        break;
                    case 'cancel':
                        this._showingConnectionForm = false;
                        this._updateView();
                        break;
                }
            }
        );

        this._updateView();
    }

    private async _saveConnection(name: string, connectionString: string) {
        // TODO: Implement actual connection saving logic
        await vscode.window.showInformationMessage(`Connection "${name}" saved!`);
    }

    private _updateView() {
        this._view.webview.html = this._getHtmlContent();
    }
Enter fullscreen mode

Exit fullscreen mode

And just register the view:

    const provider = new class implements vscode.WebviewViewProvider {
        resolveWebviewView(webviewView: vscode.WebviewView) {
            new DBChatPanel(webviewView, context);
        }
    };

    context.subscriptions.push(
        vscode.window.registerWebviewViewProvider('dbchat.chatPanel', provider)
    );
Enter fullscreen mode

Exit fullscreen mode



The Result – A Small Demo



Next Steps

Now that we have a dummy chat & connections UI, the next step would be to make them dynamic.

First – we want the connections UI to manipulate the ~/.dbchat.toml file.

Second – we want the chat queries forwarded to the backend via LSP and the response displayed appropriately in the frontend.

Follow me at @shrsv23 for more updates.



Source link
lol

By stp2y

Leave a Reply

Your email address will not be published. Required fields are marked *

No widgets found. Go to Widget page and add the widget in Offcanvas Sidebar Widget Area.