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"

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"
  }
}

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>
        `;

(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>
        `;

(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>

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();
    }

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)
    );

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.

Author Of article : Shrijith Venkatramana Read full article